@poleski/quality-tools 0.1.3
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/CHANGELOG.md +13 -0
- package/LICENSE +21 -0
- package/README.md +269 -0
- package/dist/cli/main.js +4826 -0
- package/package.json +62 -0
- package/stryker/quality-tools-vitest-runner.mjs +83 -0
- package/stryker.config.cjs +65 -0
package/dist/cli/main.js
ADDED
|
@@ -0,0 +1,4826 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/shared/flagValue.ts
|
|
4
|
+
function flagValue(args2, name) {
|
|
5
|
+
const inlineFlag = args2.find((arg) => arg.startsWith(`${name}=`));
|
|
6
|
+
if (inlineFlag) {
|
|
7
|
+
return inlineFlag.slice(name.length + 1);
|
|
8
|
+
}
|
|
9
|
+
const flagIndex = args2.indexOf(name);
|
|
10
|
+
return flagIndex >= 0 ? args2[flagIndex + 1] : void 0;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// src/shared/parseTarget.ts
|
|
14
|
+
function parseTargetArg(args2, valueFlags) {
|
|
15
|
+
const flags = valueFlags;
|
|
16
|
+
for (let index = 0; index < args2.length; index += 1) {
|
|
17
|
+
const arg = args2[index];
|
|
18
|
+
if (!arg.startsWith("--")) {
|
|
19
|
+
return arg;
|
|
20
|
+
}
|
|
21
|
+
const nextArg = args2[index + 1];
|
|
22
|
+
if (flags.includes(arg) && nextArg !== void 0 && !nextArg.startsWith("--")) {
|
|
23
|
+
index += 1;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return void 0;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// src/shared/cliArgs.ts
|
|
30
|
+
function cleanCliArgs(args2) {
|
|
31
|
+
return args2.filter((arg) => arg !== "--");
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// src/shared/resolve/repoRoot.ts
|
|
35
|
+
import { resolve as resolve3 } from "node:path";
|
|
36
|
+
|
|
37
|
+
// src/shared/resolve/moduleDirectory.ts
|
|
38
|
+
import { dirname } from "node:path";
|
|
39
|
+
import { fileURLToPath } from "node:url";
|
|
40
|
+
function moduleDirectory(moduleUrl) {
|
|
41
|
+
if (!moduleUrl) {
|
|
42
|
+
return void 0;
|
|
43
|
+
}
|
|
44
|
+
if (moduleUrl.startsWith("file:")) {
|
|
45
|
+
return dirname(fileURLToPath(moduleUrl));
|
|
46
|
+
}
|
|
47
|
+
if (moduleUrl.startsWith("/")) {
|
|
48
|
+
return dirname(moduleUrl);
|
|
49
|
+
}
|
|
50
|
+
return void 0;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// src/shared/resolve/packageRoot.ts
|
|
54
|
+
import { existsSync } from "node:fs";
|
|
55
|
+
import { dirname as dirname2, join, resolve } from "node:path";
|
|
56
|
+
function packageRootFrom(repoRoot, start) {
|
|
57
|
+
if (!start) {
|
|
58
|
+
return void 0;
|
|
59
|
+
}
|
|
60
|
+
for (let currentDirectory = resolve(start), previousDirectory; currentDirectory !== previousDirectory; previousDirectory = currentDirectory, currentDirectory = dirname2(currentDirectory)) {
|
|
61
|
+
if (repoRoot && currentDirectory === repoRoot) {
|
|
62
|
+
return void 0;
|
|
63
|
+
}
|
|
64
|
+
if (existsSync(join(currentDirectory, "package.json"))) {
|
|
65
|
+
return currentDirectory;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return void 0;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// src/shared/resolve/workspaceRoot.ts
|
|
72
|
+
import { existsSync as existsSync2 } from "node:fs";
|
|
73
|
+
import { dirname as dirname3, join as join2, resolve as resolve2 } from "node:path";
|
|
74
|
+
var STRONG_ROOT_MARKERS = ["quality.config.json", "pnpm-workspace.yaml"];
|
|
75
|
+
var FALLBACK_ROOT_MARKERS = ["package.json", ".git"];
|
|
76
|
+
function workspaceRootFrom(start) {
|
|
77
|
+
if (!start) {
|
|
78
|
+
return void 0;
|
|
79
|
+
}
|
|
80
|
+
let fallbackRoot;
|
|
81
|
+
for (let currentDirectory = resolve2(start), previousDirectory = ""; currentDirectory !== previousDirectory; previousDirectory = currentDirectory, currentDirectory = dirname3(currentDirectory)) {
|
|
82
|
+
if (STRONG_ROOT_MARKERS.some((marker) => existsSync2(join2(currentDirectory, marker)))) {
|
|
83
|
+
return currentDirectory;
|
|
84
|
+
}
|
|
85
|
+
if (!fallbackRoot && FALLBACK_ROOT_MARKERS.some((marker) => existsSync2(join2(currentDirectory, marker)))) {
|
|
86
|
+
fallbackRoot = currentDirectory;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return fallbackRoot;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// src/shared/resolve/repoRoot.ts
|
|
93
|
+
function envRepoRoot(env) {
|
|
94
|
+
const configuredRoot = env.TEST_REPO_ROOT ?? env.QUALITY_TOOLS_ROOT ?? env.GITHUB_WORKSPACE;
|
|
95
|
+
return configuredRoot ? resolve3(configuredRoot) : void 0;
|
|
96
|
+
}
|
|
97
|
+
function resolveRepoRoot(options = {}) {
|
|
98
|
+
const cwd = options.cwd ?? process.cwd();
|
|
99
|
+
const env = options.env ?? process.env;
|
|
100
|
+
const configuredRepoRoot = envRepoRoot(env);
|
|
101
|
+
if (configuredRepoRoot) {
|
|
102
|
+
return configuredRepoRoot;
|
|
103
|
+
}
|
|
104
|
+
const cwdWorkspaceRoot = workspaceRootFrom(cwd);
|
|
105
|
+
if (cwdWorkspaceRoot) {
|
|
106
|
+
return cwdWorkspaceRoot;
|
|
107
|
+
}
|
|
108
|
+
throw new Error(`Unable to resolve project root from cwd "${cwd}"`);
|
|
109
|
+
}
|
|
110
|
+
function resolvePackageRoot(options = {}) {
|
|
111
|
+
const moduleUrl = options.moduleUrl ?? import.meta.url;
|
|
112
|
+
const modulePath = moduleDirectory(moduleUrl);
|
|
113
|
+
const discoveredPackageRoot = packageRootFrom(void 0, modulePath);
|
|
114
|
+
if (discoveredPackageRoot) {
|
|
115
|
+
return discoveredPackageRoot;
|
|
116
|
+
}
|
|
117
|
+
throw new Error(`Unable to resolve quality-tools package root from module URL "${moduleUrl}"`);
|
|
118
|
+
}
|
|
119
|
+
var REPO_ROOT = resolveRepoRoot();
|
|
120
|
+
var PACKAGE_ROOT = resolvePackageRoot();
|
|
121
|
+
|
|
122
|
+
// src/shared/util/pathUtils.ts
|
|
123
|
+
import { relative, sep } from "path";
|
|
124
|
+
function toPosix(value) {
|
|
125
|
+
return value.replace(/\\/g, "/").split(sep).join("/");
|
|
126
|
+
}
|
|
127
|
+
function relativeTo(root, value) {
|
|
128
|
+
return toPosix(relative(root, value));
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// src/shared/util/packageTarget.ts
|
|
132
|
+
function findContainingPackage(absolutePath, workspacePackages) {
|
|
133
|
+
return workspacePackages.filter((workspacePackage) => absolutePath === workspacePackage.root || absolutePath.startsWith(`${workspacePackage.root}/`)).sort((left, right) => right.root.length - left.root.length)[0];
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// src/shared/resolve/path.ts
|
|
137
|
+
import { existsSync as existsSync4, statSync } from "fs";
|
|
138
|
+
import { isAbsolute, resolve as resolve4 } from "path";
|
|
139
|
+
|
|
140
|
+
// src/shared/util/workspacePackages.ts
|
|
141
|
+
import { existsSync as existsSync3, readFileSync } from "fs";
|
|
142
|
+
import { basename, dirname as dirname4, join as join3 } from "path";
|
|
143
|
+
import { globSync } from "glob";
|
|
144
|
+
function workspacePackageFromPackageJson(repoRoot, relativePackageJson) {
|
|
145
|
+
const packageJsonPath = join3(repoRoot, relativePackageJson);
|
|
146
|
+
const manifest = JSON.parse(readFileSync(packageJsonPath, "utf-8"));
|
|
147
|
+
const relativeRoot = toPosix(dirname4(relativePackageJson));
|
|
148
|
+
return {
|
|
149
|
+
...manifest.name ? { manifestName: manifest.name } : {},
|
|
150
|
+
name: manifest.name?.split("/").pop() ?? basename(dirname4(packageJsonPath)),
|
|
151
|
+
relativeRoot,
|
|
152
|
+
root: join3(repoRoot, relativeRoot)
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
function pnpmWorkspaceGlobs(repoRoot) {
|
|
156
|
+
const workspacePath = join3(repoRoot, "pnpm-workspace.yaml");
|
|
157
|
+
if (!existsSync3(workspacePath)) {
|
|
158
|
+
return [];
|
|
159
|
+
}
|
|
160
|
+
const lines = readFileSync(workspacePath, "utf-8").split(/\r?\n/);
|
|
161
|
+
const packageGlobs = [];
|
|
162
|
+
let inPackagesBlock = false;
|
|
163
|
+
for (const line of lines) {
|
|
164
|
+
if (/^\s*packages\s*:/.test(line)) {
|
|
165
|
+
inPackagesBlock = true;
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
if (!inPackagesBlock) {
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
171
|
+
if (/^\S/.test(line)) {
|
|
172
|
+
break;
|
|
173
|
+
}
|
|
174
|
+
const match = line.match(/^\s*-\s*['"]?([^'"]+)['"]?\s*$/);
|
|
175
|
+
if (match) {
|
|
176
|
+
packageGlobs.push(match[1]);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
return packageGlobs;
|
|
180
|
+
}
|
|
181
|
+
function packageJsonWorkspaceGlobs(repoRoot) {
|
|
182
|
+
const packageJsonPath = join3(repoRoot, "package.json");
|
|
183
|
+
if (!existsSync3(packageJsonPath)) {
|
|
184
|
+
return [];
|
|
185
|
+
}
|
|
186
|
+
const manifest = JSON.parse(readFileSync(packageJsonPath, "utf-8"));
|
|
187
|
+
if (Array.isArray(manifest.workspaces)) {
|
|
188
|
+
return manifest.workspaces;
|
|
189
|
+
}
|
|
190
|
+
return manifest.workspaces?.packages ?? [];
|
|
191
|
+
}
|
|
192
|
+
function workspaceGlobs(repoRoot) {
|
|
193
|
+
return pnpmWorkspaceGlobs(repoRoot).length > 0 ? pnpmWorkspaceGlobs(repoRoot) : packageJsonWorkspaceGlobs(repoRoot);
|
|
194
|
+
}
|
|
195
|
+
function packageJsonGlob(pattern) {
|
|
196
|
+
const normalizedPattern = toPosix(pattern).replace(/\/$/, "");
|
|
197
|
+
return normalizedPattern.endsWith("/package.json") ? normalizedPattern : `${normalizedPattern}/package.json`;
|
|
198
|
+
}
|
|
199
|
+
function discoverWorkspacePackageJsons(repoRoot) {
|
|
200
|
+
const positiveGlobs = workspaceGlobs(repoRoot).filter((pattern) => !pattern.startsWith("!"));
|
|
201
|
+
const negativeGlobs = workspaceGlobs(repoRoot).filter((pattern) => pattern.startsWith("!")).map((pattern) => packageJsonGlob(pattern.slice(1)));
|
|
202
|
+
return [...new Set(
|
|
203
|
+
positiveGlobs.flatMap((pattern) => globSync(packageJsonGlob(pattern), {
|
|
204
|
+
cwd: repoRoot,
|
|
205
|
+
dot: true,
|
|
206
|
+
ignore: ["**/node_modules/**", ...negativeGlobs],
|
|
207
|
+
nodir: true,
|
|
208
|
+
posix: true
|
|
209
|
+
}))
|
|
210
|
+
)].sort();
|
|
211
|
+
}
|
|
212
|
+
function shouldIncludeRootPackage(repoRoot, packageJsons) {
|
|
213
|
+
return existsSync3(join3(repoRoot, "package.json")) && (packageJsons.length === 0 || existsSync3(join3(repoRoot, "src")) || existsSync3(join3(repoRoot, "tests")));
|
|
214
|
+
}
|
|
215
|
+
function listWorkspacePackages(repoRoot) {
|
|
216
|
+
const packages = [];
|
|
217
|
+
const packageJsons = discoverWorkspacePackageJsons(repoRoot);
|
|
218
|
+
if (shouldIncludeRootPackage(repoRoot, packageJsons)) {
|
|
219
|
+
const packageJson = JSON.parse(readFileSync(join3(repoRoot, "package.json"), "utf-8"));
|
|
220
|
+
packages.push({
|
|
221
|
+
...packageJson.name ? { manifestName: packageJson.name } : {},
|
|
222
|
+
name: packageJson.name?.split("/").pop() ?? "root",
|
|
223
|
+
relativeRoot: ".",
|
|
224
|
+
root: repoRoot
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
return [
|
|
228
|
+
...packages,
|
|
229
|
+
...packageJsons.map((packageJson) => workspacePackageFromPackageJson(repoRoot, packageJson))
|
|
230
|
+
].sort((left, right) => left.name.localeCompare(right.name));
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// src/shared/resolve/path.ts
|
|
234
|
+
function packagePathCandidates(repoRoot, input) {
|
|
235
|
+
const normalizedInput = input.replace(/\/+$/, "");
|
|
236
|
+
return listWorkspacePackages(repoRoot).flatMap((workspacePackage) => {
|
|
237
|
+
const aliases = [
|
|
238
|
+
workspacePackage.name,
|
|
239
|
+
...workspacePackage.manifestName ? [workspacePackage.manifestName] : []
|
|
240
|
+
];
|
|
241
|
+
return aliases.flatMap((alias) => {
|
|
242
|
+
if (normalizedInput === alias) {
|
|
243
|
+
return [workspacePackage.root];
|
|
244
|
+
}
|
|
245
|
+
if (normalizedInput.startsWith(`${alias}/`)) {
|
|
246
|
+
return [resolve4(workspacePackage.root, normalizedInput.slice(alias.length + 1))];
|
|
247
|
+
}
|
|
248
|
+
return [];
|
|
249
|
+
});
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
function resolveExistingPath(repoRoot, input) {
|
|
253
|
+
if (!input) {
|
|
254
|
+
return repoRoot;
|
|
255
|
+
}
|
|
256
|
+
const candidates = [
|
|
257
|
+
isAbsolute(input) ? input : resolve4(repoRoot, input),
|
|
258
|
+
...packagePathCandidates(repoRoot, input)
|
|
259
|
+
];
|
|
260
|
+
const found = candidates.find((candidate) => existsSync4(candidate));
|
|
261
|
+
if (!found) {
|
|
262
|
+
throw new Error(`Target not found: ${input}`);
|
|
263
|
+
}
|
|
264
|
+
return found;
|
|
265
|
+
}
|
|
266
|
+
function pathKind(absolutePath) {
|
|
267
|
+
return statSync(absolutePath).isDirectory() ? "directory" : "file";
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// src/shared/resolve/target.ts
|
|
271
|
+
function resolveQualityTarget(repoRoot, input) {
|
|
272
|
+
const workspacePackages = listWorkspacePackages(repoRoot);
|
|
273
|
+
const normalizedInput = input?.replace(/\/+$/, "");
|
|
274
|
+
const explicitPackage = normalizedInput ? workspacePackages.find((workspacePackage2) => {
|
|
275
|
+
const aliases = [
|
|
276
|
+
workspacePackage2.name,
|
|
277
|
+
...workspacePackage2.manifestName ? [workspacePackage2.manifestName] : []
|
|
278
|
+
];
|
|
279
|
+
return aliases.includes(normalizedInput);
|
|
280
|
+
}) : void 0;
|
|
281
|
+
const absolutePath = resolveExistingPath(repoRoot, input);
|
|
282
|
+
if (absolutePath === repoRoot) {
|
|
283
|
+
if (explicitPackage) {
|
|
284
|
+
return {
|
|
285
|
+
absolutePath,
|
|
286
|
+
kind: "package",
|
|
287
|
+
relativePath: ".",
|
|
288
|
+
packageName: explicitPackage.name,
|
|
289
|
+
packageRelativePath: ".",
|
|
290
|
+
packageRoot: explicitPackage.root
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
return {
|
|
294
|
+
absolutePath,
|
|
295
|
+
kind: "repo",
|
|
296
|
+
relativePath: "."
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
const workspacePackage = findContainingPackage(absolutePath, workspacePackages);
|
|
300
|
+
const relativePath = relativeTo(repoRoot, absolutePath);
|
|
301
|
+
const kind = pathKind(absolutePath);
|
|
302
|
+
if (!workspacePackage) {
|
|
303
|
+
return {
|
|
304
|
+
absolutePath,
|
|
305
|
+
kind,
|
|
306
|
+
relativePath
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
if (absolutePath === workspacePackage.root) {
|
|
310
|
+
return {
|
|
311
|
+
absolutePath,
|
|
312
|
+
kind: "package",
|
|
313
|
+
relativePath,
|
|
314
|
+
packageName: workspacePackage.name,
|
|
315
|
+
packageRelativePath: ".",
|
|
316
|
+
packageRoot: workspacePackage.root
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
return {
|
|
320
|
+
absolutePath,
|
|
321
|
+
kind,
|
|
322
|
+
relativePath,
|
|
323
|
+
packageName: workspacePackage.name,
|
|
324
|
+
packageRelativePath: relativeTo(workspacePackage.root, absolutePath),
|
|
325
|
+
packageRoot: workspacePackage.root
|
|
326
|
+
};
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// src/boundaries/analyze.ts
|
|
330
|
+
import { existsSync as existsSync5 } from "fs";
|
|
331
|
+
import { basename as basename3, join as join8 } from "path";
|
|
332
|
+
|
|
333
|
+
// src/boundaries/graph/packageAnalysis.ts
|
|
334
|
+
import { basename as basename2 } from "path";
|
|
335
|
+
|
|
336
|
+
// src/organize/cohesion/imports/parse.ts
|
|
337
|
+
import { readFileSync as readFileSync2 } from "fs";
|
|
338
|
+
import * as ts2 from "typescript";
|
|
339
|
+
|
|
340
|
+
// src/organize/cohesion/imports/scriptKind.ts
|
|
341
|
+
import * as ts from "typescript";
|
|
342
|
+
function getScriptKind(fileName) {
|
|
343
|
+
if (fileName.endsWith(".jsx")) {
|
|
344
|
+
return ts.ScriptKind.JSX;
|
|
345
|
+
}
|
|
346
|
+
if (fileName.endsWith(".js")) {
|
|
347
|
+
return ts.ScriptKind.JS;
|
|
348
|
+
}
|
|
349
|
+
if (fileName.endsWith(".tsx")) {
|
|
350
|
+
return ts.ScriptKind.TSX;
|
|
351
|
+
}
|
|
352
|
+
return ts.ScriptKind.TS;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// src/organize/cohesion/imports/parse.ts
|
|
356
|
+
function extractImports(sourceFile) {
|
|
357
|
+
const imports = [];
|
|
358
|
+
function visit(node) {
|
|
359
|
+
if (ts2.isImportDeclaration(node)) {
|
|
360
|
+
const moduleSpecifier = node.moduleSpecifier;
|
|
361
|
+
if (moduleSpecifier && ts2.isStringLiteral(moduleSpecifier)) {
|
|
362
|
+
imports.push(moduleSpecifier.text);
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
if (ts2.isExportDeclaration(node)) {
|
|
366
|
+
const moduleSpecifier = node.moduleSpecifier;
|
|
367
|
+
if (moduleSpecifier && ts2.isStringLiteral(moduleSpecifier)) {
|
|
368
|
+
imports.push(moduleSpecifier.text);
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
ts2.forEachChild(node, visit);
|
|
372
|
+
}
|
|
373
|
+
visit(sourceFile);
|
|
374
|
+
return imports;
|
|
375
|
+
}
|
|
376
|
+
function parseFileImports(filePath, fileName) {
|
|
377
|
+
try {
|
|
378
|
+
const fileContent = readFileSync2(filePath, "utf-8");
|
|
379
|
+
const scriptKind = getScriptKind(fileName);
|
|
380
|
+
const sourceFile = ts2.createSourceFile(
|
|
381
|
+
fileName,
|
|
382
|
+
fileContent,
|
|
383
|
+
ts2.ScriptTarget.Latest,
|
|
384
|
+
void 0,
|
|
385
|
+
scriptKind
|
|
386
|
+
);
|
|
387
|
+
return extractImports(sourceFile);
|
|
388
|
+
} catch {
|
|
389
|
+
return [];
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
// src/boundaries/graph/imports.ts
|
|
394
|
+
import { dirname as dirname5, join as join4 } from "path";
|
|
395
|
+
function resolveImportTarget(fromFile, specifier, candidatePaths) {
|
|
396
|
+
if (specifier[0] !== ".") {
|
|
397
|
+
return void 0;
|
|
398
|
+
}
|
|
399
|
+
const basePath = join4(dirname5(fromFile), specifier);
|
|
400
|
+
const candidates = [
|
|
401
|
+
basePath,
|
|
402
|
+
`${basePath}.ts`,
|
|
403
|
+
`${basePath}.tsx`,
|
|
404
|
+
`${basePath}.js`,
|
|
405
|
+
`${basePath}.jsx`,
|
|
406
|
+
join4(basePath, "index.ts"),
|
|
407
|
+
join4(basePath, "index.tsx"),
|
|
408
|
+
join4(basePath, "index.js"),
|
|
409
|
+
join4(basePath, "index.jsx")
|
|
410
|
+
];
|
|
411
|
+
return candidates.find((candidate) => candidatePaths.has(candidate));
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
// src/config/quality.ts
|
|
415
|
+
import { readFileSync as readFileSync3 } from "fs";
|
|
416
|
+
import { isAbsolute as isAbsolute2, join as join6, matchesGlob, relative as relative2, resolve as resolve5 } from "path";
|
|
417
|
+
|
|
418
|
+
// src/config/patterns.ts
|
|
419
|
+
import { join as join5 } from "path";
|
|
420
|
+
function normalizePatterns(patterns) {
|
|
421
|
+
return (patterns ?? []).map(toPosix);
|
|
422
|
+
}
|
|
423
|
+
function mergeToolPatterns(defaults, overrides) {
|
|
424
|
+
return {
|
|
425
|
+
exclude: [...normalizePatterns(defaults?.exclude), ...normalizePatterns(overrides?.exclude)],
|
|
426
|
+
include: [...normalizePatterns(defaults?.include), ...normalizePatterns(overrides?.include)]
|
|
427
|
+
};
|
|
428
|
+
}
|
|
429
|
+
function mergeBoundaryPatterns(defaults, overrides) {
|
|
430
|
+
return {
|
|
431
|
+
exclude: [...normalizePatterns(defaults?.exclude), ...normalizePatterns(overrides?.exclude)],
|
|
432
|
+
include: [...normalizePatterns(defaults?.include), ...normalizePatterns(overrides?.include)],
|
|
433
|
+
entrypoints: [
|
|
434
|
+
...normalizePatterns(defaults?.entrypoints),
|
|
435
|
+
...normalizePatterns(overrides?.entrypoints)
|
|
436
|
+
],
|
|
437
|
+
layers: overrides?.layers ?? defaults?.layers ?? []
|
|
438
|
+
};
|
|
439
|
+
}
|
|
440
|
+
function packageRootPattern(packageRelativeRoot2, pattern) {
|
|
441
|
+
if (packageRelativeRoot2 === ".") {
|
|
442
|
+
return toPosix(pattern);
|
|
443
|
+
}
|
|
444
|
+
return toPosix(join5(packageRelativeRoot2, pattern));
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
// src/config/quality.ts
|
|
448
|
+
var CONFIG_FILE = "quality.config.json";
|
|
449
|
+
var DEFAULT_REPORTS_DIR = "reports/quality-tools";
|
|
450
|
+
function loadQualityConfig(repoRoot) {
|
|
451
|
+
const configPath = join6(repoRoot, CONFIG_FILE);
|
|
452
|
+
try {
|
|
453
|
+
return JSON.parse(readFileSync3(configPath, "utf-8"));
|
|
454
|
+
} catch {
|
|
455
|
+
return {};
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
function resolveFromRepoRoot(repoRoot, value) {
|
|
459
|
+
return isAbsolute2(value) ? value : resolve5(repoRoot, value);
|
|
460
|
+
}
|
|
461
|
+
function resolveReportsDir(repoRoot) {
|
|
462
|
+
const config = loadQualityConfig(repoRoot);
|
|
463
|
+
return resolveFromRepoRoot(repoRoot, config.reportsDir ?? DEFAULT_REPORTS_DIR);
|
|
464
|
+
}
|
|
465
|
+
function relativeReportsDir(repoRoot) {
|
|
466
|
+
return toPosix(relative2(repoRoot, resolveReportsDir(repoRoot)));
|
|
467
|
+
}
|
|
468
|
+
function resolveReportPath(repoRoot, ...segments) {
|
|
469
|
+
return join6(resolveReportsDir(repoRoot), ...segments);
|
|
470
|
+
}
|
|
471
|
+
function relativeReportPath(repoRoot, ...segments) {
|
|
472
|
+
return toPosix(relative2(repoRoot, resolveReportPath(repoRoot, ...segments)));
|
|
473
|
+
}
|
|
474
|
+
function resolvePackageToolPatterns(repoRoot, packageName, toolName) {
|
|
475
|
+
const config = loadQualityConfig(repoRoot);
|
|
476
|
+
return mergeToolPatterns(config.defaults?.[toolName], config.packages?.[packageName]?.[toolName]);
|
|
477
|
+
}
|
|
478
|
+
function resolveDefaultToolPatterns(repoRoot, toolName) {
|
|
479
|
+
const config = loadQualityConfig(repoRoot);
|
|
480
|
+
return mergeToolPatterns(config.defaults?.[toolName], void 0);
|
|
481
|
+
}
|
|
482
|
+
function resolvePackageToolGlobs(repoRoot, packageName, toolName) {
|
|
483
|
+
const patterns = resolvePackageToolPatterns(repoRoot, packageName, toolName);
|
|
484
|
+
const workspacePackage = listWorkspacePackages(repoRoot).find((entry) => entry.name === packageName);
|
|
485
|
+
const packageRelativeRoot2 = workspacePackage?.relativeRoot ?? packageName;
|
|
486
|
+
return {
|
|
487
|
+
exclude: patterns.exclude.map((pattern) => packageRootPattern(packageRelativeRoot2, pattern)),
|
|
488
|
+
include: patterns.include.map((pattern) => packageRootPattern(packageRelativeRoot2, pattern))
|
|
489
|
+
};
|
|
490
|
+
}
|
|
491
|
+
function resolvePackageCrapCoverage(repoRoot, packageName) {
|
|
492
|
+
const config = loadQualityConfig(repoRoot);
|
|
493
|
+
const coverage = packageName ? config.packages?.[packageName]?.crap?.coverage ?? config.defaults?.crap?.coverage : config.defaults?.crap?.coverage;
|
|
494
|
+
if (!coverage) {
|
|
495
|
+
return [];
|
|
496
|
+
}
|
|
497
|
+
return Array.isArray(coverage) ? coverage : [coverage];
|
|
498
|
+
}
|
|
499
|
+
function resolveMutationStrykerConfig(repoRoot, packageName) {
|
|
500
|
+
const config = loadQualityConfig(repoRoot);
|
|
501
|
+
const configuredPath = packageName ? config.packages?.[packageName]?.mutation?.strykerConfig ?? config.defaults?.mutation?.strykerConfig : config.defaults?.mutation?.strykerConfig;
|
|
502
|
+
return configuredPath ? resolveFromRepoRoot(repoRoot, configuredPath) : void 0;
|
|
503
|
+
}
|
|
504
|
+
function resolvePackageBoundaryConfig(repoRoot, packageName) {
|
|
505
|
+
const config = loadQualityConfig(repoRoot);
|
|
506
|
+
return mergeBoundaryPatterns(config.defaults?.boundaries, config.packages?.[packageName]?.boundaries);
|
|
507
|
+
}
|
|
508
|
+
function pathIncludedByTool(repoRoot, packageName, toolName, packageRelativePath) {
|
|
509
|
+
const patterns = resolvePackageToolPatterns(repoRoot, packageName, toolName);
|
|
510
|
+
const normalizedPath = toPosix(packageRelativePath);
|
|
511
|
+
const included = patterns.include.length === 0 || patterns.include.some((pattern) => matchesGlob(normalizedPath, pattern));
|
|
512
|
+
const excluded = patterns.exclude.some((pattern) => matchesGlob(normalizedPath, pattern));
|
|
513
|
+
return included && !excluded;
|
|
514
|
+
}
|
|
515
|
+
function pathIncludedByDefaultTool(repoRoot, toolName, repoRelativePath) {
|
|
516
|
+
const patterns = resolveDefaultToolPatterns(repoRoot, toolName);
|
|
517
|
+
const normalizedPath = toPosix(repoRelativePath);
|
|
518
|
+
const included = patterns.include.length === 0 || patterns.include.some((pattern) => matchesGlob(normalizedPath, pattern));
|
|
519
|
+
const excluded = patterns.exclude.some((pattern) => matchesGlob(normalizedPath, pattern));
|
|
520
|
+
return included && !excluded;
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
// src/boundaries/graph/node.ts
|
|
524
|
+
import { matchesGlob as matchesGlob2 } from "path";
|
|
525
|
+
function layerForPath(packageRelativePath, layers) {
|
|
526
|
+
return layers.find((layer) => layer.include.some((pattern) => matchesGlob2(packageRelativePath, pattern)));
|
|
527
|
+
}
|
|
528
|
+
function isEntrypoint(packageRelativePath, entrypoints) {
|
|
529
|
+
return entrypoints.some((pattern) => matchesGlob2(packageRelativePath, pattern));
|
|
530
|
+
}
|
|
531
|
+
function createNode(absolutePath, packageName, packageRelativePath, relativePath, layers, entrypoints) {
|
|
532
|
+
const layer = layerForPath(packageRelativePath, layers);
|
|
533
|
+
return {
|
|
534
|
+
absolutePath,
|
|
535
|
+
entrypoint: isEntrypoint(packageRelativePath, entrypoints),
|
|
536
|
+
incoming: 0,
|
|
537
|
+
layer: layer?.name,
|
|
538
|
+
outgoing: 0,
|
|
539
|
+
packageName,
|
|
540
|
+
packageRelativePath,
|
|
541
|
+
relativePath
|
|
542
|
+
};
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
// src/boundaries/graph/selection.ts
|
|
546
|
+
import { join as join7, matchesGlob as matchesGlob3 } from "path";
|
|
547
|
+
|
|
548
|
+
// src/organize/metric/walk/scan.ts
|
|
549
|
+
import { readdirSync } from "fs";
|
|
550
|
+
import { resolve as resolve6 } from "path";
|
|
551
|
+
|
|
552
|
+
// src/organize/metric/walk/filters.ts
|
|
553
|
+
function isHidden(name) {
|
|
554
|
+
return name.startsWith(".");
|
|
555
|
+
}
|
|
556
|
+
function isExcludedDirectory(name) {
|
|
557
|
+
if (isHidden(name)) {
|
|
558
|
+
return true;
|
|
559
|
+
}
|
|
560
|
+
return name === "node_modules" || name === "coverage" || name === "reports" || name === "dist" || name === "dist-e2e";
|
|
561
|
+
}
|
|
562
|
+
function isTypeScriptOrJavaScriptFile(name) {
|
|
563
|
+
return /\.(ts|tsx|js|jsx)$/.test(name);
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
// src/organize/metric/walk/sort.ts
|
|
567
|
+
function sortDirectoryNames(names) {
|
|
568
|
+
return [...names].sort();
|
|
569
|
+
}
|
|
570
|
+
function sortDirectoryEntries(entries) {
|
|
571
|
+
return [...entries].sort((left, right) => left.directoryPath.localeCompare(right.directoryPath));
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
// src/organize/metric/walk/scan.ts
|
|
575
|
+
function scanDirectory(directoryPath) {
|
|
576
|
+
const items = readdirSync(directoryPath, { withFileTypes: true });
|
|
577
|
+
const files = [];
|
|
578
|
+
const subdirectories = [];
|
|
579
|
+
for (const item of items) {
|
|
580
|
+
if (item.isFile() && isTypeScriptOrJavaScriptFile(item.name)) {
|
|
581
|
+
files.push(item.name);
|
|
582
|
+
} else if (item.isDirectory() && !isExcludedDirectory(item.name)) {
|
|
583
|
+
subdirectories.push(item.name);
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
return {
|
|
587
|
+
files: sortDirectoryNames(files),
|
|
588
|
+
subdirectories: sortDirectoryNames(subdirectories)
|
|
589
|
+
};
|
|
590
|
+
}
|
|
591
|
+
function walkDirectoriesRecursive(directoryPath, entries) {
|
|
592
|
+
const entry = scanDirectory(directoryPath);
|
|
593
|
+
entries.push({
|
|
594
|
+
directoryPath,
|
|
595
|
+
files: entry.files,
|
|
596
|
+
subdirectories: entry.subdirectories
|
|
597
|
+
});
|
|
598
|
+
for (const subdirectory of entry.subdirectories) {
|
|
599
|
+
walkDirectoriesRecursive(resolve6(directoryPath, subdirectory), entries);
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
// src/organize/metric/directoryWalk.ts
|
|
604
|
+
function walkDirectories(rootPath) {
|
|
605
|
+
const entries = [];
|
|
606
|
+
walkDirectoriesRecursive(rootPath, entries);
|
|
607
|
+
return sortDirectoryEntries(entries);
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
// src/boundaries/graph/selection.ts
|
|
611
|
+
function isBoundaryEntrypoint(repoRoot, packageName, packageRelativePath) {
|
|
612
|
+
const { entrypoints } = resolvePackageBoundaryConfig(repoRoot, packageName);
|
|
613
|
+
return entrypoints.some((pattern) => matchesGlob3(packageRelativePath, pattern));
|
|
614
|
+
}
|
|
615
|
+
function resolvePackageCandidates(repoRoot, workspacePackage) {
|
|
616
|
+
const entries = walkDirectories(workspacePackage.root);
|
|
617
|
+
const selected = [];
|
|
618
|
+
for (const entry of entries) {
|
|
619
|
+
for (const fileName of entry.files) {
|
|
620
|
+
const absolutePath = join7(entry.directoryPath, fileName);
|
|
621
|
+
const packageRelativePath = toPosix(absolutePath.slice(workspacePackage.root.length + 1));
|
|
622
|
+
if (isBoundaryEntrypoint(repoRoot, workspacePackage.name, packageRelativePath) || pathIncludedByTool(
|
|
623
|
+
repoRoot,
|
|
624
|
+
workspacePackage.name,
|
|
625
|
+
"boundaries",
|
|
626
|
+
packageRelativePath
|
|
627
|
+
)) {
|
|
628
|
+
selected.push(absolutePath);
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
return selected;
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
// src/boundaries/graph/nodeIndex.ts
|
|
636
|
+
function createNodesByPath(repoRoot, workspacePackage) {
|
|
637
|
+
const config = resolvePackageBoundaryConfig(repoRoot, workspacePackage.name);
|
|
638
|
+
const selectedPaths = resolvePackageCandidates(repoRoot, workspacePackage);
|
|
639
|
+
const nodesByPath = new Map(
|
|
640
|
+
selectedPaths.map((absolutePath) => {
|
|
641
|
+
const packageRelativePath = toPosix(absolutePath.slice(workspacePackage.root.length + 1));
|
|
642
|
+
const relativePath = toPosix(absolutePath.slice(repoRoot.length + 1));
|
|
643
|
+
const node = createNode(
|
|
644
|
+
absolutePath,
|
|
645
|
+
workspacePackage.name,
|
|
646
|
+
packageRelativePath,
|
|
647
|
+
relativePath,
|
|
648
|
+
config.layers,
|
|
649
|
+
config.entrypoints
|
|
650
|
+
);
|
|
651
|
+
node.allowedLayers = config.layers.find((layer) => layer.name === node.layer)?.allow ?? [];
|
|
652
|
+
return [absolutePath, node];
|
|
653
|
+
})
|
|
654
|
+
);
|
|
655
|
+
return {
|
|
656
|
+
candidatePaths: new Set(selectedPaths),
|
|
657
|
+
nodesByPath
|
|
658
|
+
};
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
// src/boundaries/graph/deadFiles.ts
|
|
662
|
+
function deadEnds(files) {
|
|
663
|
+
return files.filter((file) => file.incoming === 0 && file.outgoing === 0 && !file.entrypoint);
|
|
664
|
+
}
|
|
665
|
+
function deadSurfaces(files) {
|
|
666
|
+
return files.filter((file) => file.incoming === 0 && file.outgoing > 0 && !file.entrypoint);
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
// src/boundaries/graph/scope.ts
|
|
670
|
+
function fileIsInsideScope(filePath, scope) {
|
|
671
|
+
if (!scope) {
|
|
672
|
+
return true;
|
|
673
|
+
}
|
|
674
|
+
return filePath === scope.relativePath || filePath.startsWith(`${scope.relativePath}/`);
|
|
675
|
+
}
|
|
676
|
+
function selectedViolations(violations, files) {
|
|
677
|
+
const scopedPaths = new Set(files.map((file) => file.relativePath));
|
|
678
|
+
return violations.filter((violation) => scopedPaths.has(violation.from));
|
|
679
|
+
}
|
|
680
|
+
function selectedFiles(files, scope) {
|
|
681
|
+
return files.filter((file) => fileIsInsideScope(file.relativePath, scope));
|
|
682
|
+
}
|
|
683
|
+
function createScopedReport(workspacePackage, files, violations, scope) {
|
|
684
|
+
return {
|
|
685
|
+
deadEnds: deadEnds(files),
|
|
686
|
+
deadSurfaces: deadSurfaces(files),
|
|
687
|
+
files,
|
|
688
|
+
layerViolations: selectedViolations(violations, files),
|
|
689
|
+
target: scope?.relativePath ?? workspacePackage.relativeRoot ?? workspacePackage.name
|
|
690
|
+
};
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
// src/boundaries/graph/packageAnalysis.ts
|
|
694
|
+
function* collectViolations(absolutePath, nodesByPath, candidatePaths) {
|
|
695
|
+
const node = nodesByPath.get(absolutePath);
|
|
696
|
+
const imports = parseFileImports(absolutePath, basename2(absolutePath));
|
|
697
|
+
for (const specifier of imports) {
|
|
698
|
+
const resolvedImport = resolveImportTarget(absolutePath, specifier, candidatePaths);
|
|
699
|
+
if (!resolvedImport) {
|
|
700
|
+
continue;
|
|
701
|
+
}
|
|
702
|
+
const importedNode = nodesByPath.get(resolvedImport);
|
|
703
|
+
node.outgoing += 1;
|
|
704
|
+
importedNode.incoming += 1;
|
|
705
|
+
if (node.layer && importedNode.layer && node.layer !== importedNode.layer && !node.allowedLayers.includes(importedNode.layer)) {
|
|
706
|
+
yield {
|
|
707
|
+
from: node.relativePath,
|
|
708
|
+
fromLayer: node.layer,
|
|
709
|
+
reason: `${node.layer} cannot depend on ${importedNode.layer}`,
|
|
710
|
+
to: importedNode.relativePath,
|
|
711
|
+
toLayer: importedNode.layer
|
|
712
|
+
};
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
function analyzePackage(repoRoot, workspacePackage, scope) {
|
|
717
|
+
const { candidatePaths, nodesByPath } = createNodesByPath(repoRoot, workspacePackage);
|
|
718
|
+
const violations = Array.from(candidatePaths).flatMap(
|
|
719
|
+
(absolutePath) => Array.from(collectViolations(absolutePath, nodesByPath, candidatePaths))
|
|
720
|
+
);
|
|
721
|
+
return createScopedReport(
|
|
722
|
+
workspacePackage,
|
|
723
|
+
selectedFiles([...nodesByPath.values()], scope),
|
|
724
|
+
violations,
|
|
725
|
+
scope
|
|
726
|
+
);
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
// src/boundaries/merge.ts
|
|
730
|
+
function mergeReports(target, reports) {
|
|
731
|
+
const files = [];
|
|
732
|
+
const deadEnds2 = [];
|
|
733
|
+
const deadSurfaces2 = [];
|
|
734
|
+
const layerViolations = [];
|
|
735
|
+
for (const report of reports) {
|
|
736
|
+
files.push(...report.files);
|
|
737
|
+
deadEnds2.push(...report.deadEnds);
|
|
738
|
+
deadSurfaces2.push(...report.deadSurfaces);
|
|
739
|
+
layerViolations.push(...report.layerViolations);
|
|
740
|
+
}
|
|
741
|
+
return {
|
|
742
|
+
deadEnds: deadEnds2,
|
|
743
|
+
deadSurfaces: deadSurfaces2,
|
|
744
|
+
files,
|
|
745
|
+
layerViolations,
|
|
746
|
+
target
|
|
747
|
+
};
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
// src/boundaries/analyze.ts
|
|
751
|
+
function analyzePackageRoot(repoRoot, workspacePackage, scope) {
|
|
752
|
+
return analyzePackage(repoRoot, workspacePackage, scope);
|
|
753
|
+
}
|
|
754
|
+
function analyzeBoundaries(repoRoot, target) {
|
|
755
|
+
const workspacePackages = listWorkspacePackages(repoRoot);
|
|
756
|
+
if (target.kind === "repo") {
|
|
757
|
+
return mergeReports(
|
|
758
|
+
"packages",
|
|
759
|
+
workspacePackages.map((workspacePackage) => analyzePackageRoot(repoRoot, workspacePackage))
|
|
760
|
+
);
|
|
761
|
+
}
|
|
762
|
+
if (target.packageName) {
|
|
763
|
+
const workspacePackage = workspacePackages.find((entry) => entry.name === target.packageName || entry.manifestName === target.packageName);
|
|
764
|
+
return analyzePackageRoot(repoRoot, {
|
|
765
|
+
name: target.packageName,
|
|
766
|
+
root: target.packageRoot ?? workspacePackage?.root ?? join8(repoRoot, target.packageName),
|
|
767
|
+
relativeRoot: workspacePackage?.relativeRoot
|
|
768
|
+
}, target);
|
|
769
|
+
}
|
|
770
|
+
if (existsSync5(target.absolutePath)) {
|
|
771
|
+
const workspacePackage = {
|
|
772
|
+
name: basename3(target.absolutePath),
|
|
773
|
+
root: target.absolutePath
|
|
774
|
+
};
|
|
775
|
+
return analyzePackageRoot(repoRoot, workspacePackage, target);
|
|
776
|
+
}
|
|
777
|
+
return {
|
|
778
|
+
deadEnds: [],
|
|
779
|
+
deadSurfaces: [],
|
|
780
|
+
files: [],
|
|
781
|
+
layerViolations: [],
|
|
782
|
+
target: target.relativePath
|
|
783
|
+
};
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
// src/boundaries/report/format.ts
|
|
787
|
+
function formatLayerLabel(layer) {
|
|
788
|
+
return layer ? ` [${layer}]` : "";
|
|
789
|
+
}
|
|
790
|
+
function summaryLines(report) {
|
|
791
|
+
return [
|
|
792
|
+
"",
|
|
793
|
+
`Boundaries for ${report.target}`,
|
|
794
|
+
"\u2501".repeat(72),
|
|
795
|
+
`Files: ${report.files.length}`,
|
|
796
|
+
`Layer violations: ${report.layerViolations.length}`,
|
|
797
|
+
`Dead surfaces: ${report.deadSurfaces.length}`,
|
|
798
|
+
`Dead ends: ${report.deadEnds.length}`,
|
|
799
|
+
""
|
|
800
|
+
];
|
|
801
|
+
}
|
|
802
|
+
function formatBoundaryFile(file) {
|
|
803
|
+
return `- ${file.relativePath}${formatLayerLabel(file.layer)} (in: ${file.incoming}, out: ${file.outgoing})`;
|
|
804
|
+
}
|
|
805
|
+
function formatBoundaryViolation(violation) {
|
|
806
|
+
return `- ${violation.from} [${violation.fromLayer ?? "unclassified"}] -> ${violation.to} [${violation.toLayer ?? "unclassified"}]: ${violation.reason}`;
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
// src/boundaries/report/print.ts
|
|
810
|
+
function logLines(lines) {
|
|
811
|
+
for (const line of lines) {
|
|
812
|
+
console.log(line);
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
function reportBoundarySection(title, items, formatter) {
|
|
816
|
+
if (items.length === 0) {
|
|
817
|
+
return;
|
|
818
|
+
}
|
|
819
|
+
console.log(title);
|
|
820
|
+
for (const item of items) {
|
|
821
|
+
console.log(formatter(item));
|
|
822
|
+
}
|
|
823
|
+
console.log("");
|
|
824
|
+
}
|
|
825
|
+
function reportBoundaries(report, options = {}) {
|
|
826
|
+
if (report.files.length === 0) {
|
|
827
|
+
console.log("\nNo boundary-scope files found.\n");
|
|
828
|
+
return;
|
|
829
|
+
}
|
|
830
|
+
logLines(summaryLines(report));
|
|
831
|
+
reportBoundarySection("Layer violations:", report.layerViolations, formatBoundaryViolation);
|
|
832
|
+
reportBoundarySection("Dead surfaces:", report.deadSurfaces, formatBoundaryFile);
|
|
833
|
+
reportBoundarySection("Dead ends:", report.deadEnds, formatBoundaryFile);
|
|
834
|
+
if (options.verbose) {
|
|
835
|
+
console.log("All analyzed files:");
|
|
836
|
+
for (const file of report.files) {
|
|
837
|
+
console.log(formatBoundaryFile(file));
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
// src/boundaries/command.ts
|
|
843
|
+
var DEFAULT_DEPENDENCIES = {
|
|
844
|
+
analyzeBoundaries,
|
|
845
|
+
reportBoundaries,
|
|
846
|
+
resolveQualityTarget,
|
|
847
|
+
setExitCode: (code) => {
|
|
848
|
+
process.exitCode = code;
|
|
849
|
+
}
|
|
850
|
+
};
|
|
851
|
+
function runBoundariesCli(rawArgs, dependencies = DEFAULT_DEPENDENCIES) {
|
|
852
|
+
const args2 = cleanCliArgs(rawArgs);
|
|
853
|
+
const target = dependencies.resolveQualityTarget(
|
|
854
|
+
REPO_ROOT,
|
|
855
|
+
parseTargetArg(args2, [])
|
|
856
|
+
);
|
|
857
|
+
const report = dependencies.analyzeBoundaries(REPO_ROOT, target);
|
|
858
|
+
const verbose = args2.includes("--verbose");
|
|
859
|
+
const strict = args2.includes("--strict");
|
|
860
|
+
const json = args2.includes("--json");
|
|
861
|
+
if (json) {
|
|
862
|
+
console.log(JSON.stringify(report, null, 2));
|
|
863
|
+
} else {
|
|
864
|
+
dependencies.reportBoundaries(report, { verbose });
|
|
865
|
+
}
|
|
866
|
+
const hasHardFailures = report.layerViolations.length > 0 || report.deadEnds.length > 0;
|
|
867
|
+
const hasStrictFailures = strict && report.deadSurfaces.length > 0;
|
|
868
|
+
if (hasHardFailures || hasStrictFailures) {
|
|
869
|
+
dependencies.setExitCode(1);
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
// src/crap/analysis/run.ts
|
|
874
|
+
import { existsSync as existsSync6 } from "fs";
|
|
875
|
+
import * as path2 from "path";
|
|
876
|
+
|
|
877
|
+
// src/crap/analysis/calculate.ts
|
|
878
|
+
function calculateCrap(complexity, coverage) {
|
|
879
|
+
const uncovered = 1 - coverage / 100;
|
|
880
|
+
return complexity ** 2 * uncovered ** 3 + complexity;
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
// src/crap/analysis/extractFunctions.ts
|
|
884
|
+
import * as ts6 from "typescript";
|
|
885
|
+
|
|
886
|
+
// src/crap/complexity/compute.ts
|
|
887
|
+
import * as ts3 from "typescript";
|
|
888
|
+
var BRANCHING_KINDS = /* @__PURE__ */ new Set([
|
|
889
|
+
ts3.SyntaxKind.IfStatement,
|
|
890
|
+
ts3.SyntaxKind.ForStatement,
|
|
891
|
+
ts3.SyntaxKind.ForInStatement,
|
|
892
|
+
ts3.SyntaxKind.ForOfStatement,
|
|
893
|
+
ts3.SyntaxKind.WhileStatement,
|
|
894
|
+
ts3.SyntaxKind.DoStatement,
|
|
895
|
+
ts3.SyntaxKind.CaseClause,
|
|
896
|
+
ts3.SyntaxKind.CatchClause,
|
|
897
|
+
ts3.SyntaxKind.ConditionalExpression
|
|
898
|
+
]);
|
|
899
|
+
var SHORT_CIRCUIT_OPERATORS = /* @__PURE__ */ new Set([
|
|
900
|
+
ts3.SyntaxKind.AmpersandAmpersandToken,
|
|
901
|
+
ts3.SyntaxKind.BarBarToken,
|
|
902
|
+
ts3.SyntaxKind.QuestionQuestionToken
|
|
903
|
+
]);
|
|
904
|
+
function complexityIncrement(node) {
|
|
905
|
+
if (BRANCHING_KINDS.has(node.kind)) {
|
|
906
|
+
return 1;
|
|
907
|
+
}
|
|
908
|
+
if (ts3.isBinaryExpression(node) && SHORT_CIRCUIT_OPERATORS.has(node.operatorToken.kind)) {
|
|
909
|
+
return 1;
|
|
910
|
+
}
|
|
911
|
+
return 0;
|
|
912
|
+
}
|
|
913
|
+
function walkComplexity(node) {
|
|
914
|
+
let total = complexityIncrement(node);
|
|
915
|
+
ts3.forEachChild(node, (child) => {
|
|
916
|
+
total += walkComplexity(child);
|
|
917
|
+
});
|
|
918
|
+
return total;
|
|
919
|
+
}
|
|
920
|
+
function computeComplexity(node) {
|
|
921
|
+
let total = 0;
|
|
922
|
+
ts3.forEachChild(node, (child) => {
|
|
923
|
+
total += walkComplexity(child);
|
|
924
|
+
});
|
|
925
|
+
return total + 1;
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
// src/crap/complexity/getFunctionName.ts
|
|
929
|
+
import * as ts4 from "typescript";
|
|
930
|
+
function identifierText(name) {
|
|
931
|
+
return ts4.isIdentifier(name) ? name.text : void 0;
|
|
932
|
+
}
|
|
933
|
+
function declarationName(node) {
|
|
934
|
+
if (ts4.isFunctionDeclaration(node) || ts4.isMethodDeclaration(node)) {
|
|
935
|
+
return node.name ? identifierText(node.name) : void 0;
|
|
936
|
+
}
|
|
937
|
+
return void 0;
|
|
938
|
+
}
|
|
939
|
+
function accessorName(node) {
|
|
940
|
+
if (ts4.isGetAccessorDeclaration(node)) {
|
|
941
|
+
const name = identifierText(node.name);
|
|
942
|
+
return name ? `get ${name}` : void 0;
|
|
943
|
+
}
|
|
944
|
+
if (ts4.isSetAccessorDeclaration(node)) {
|
|
945
|
+
const name = identifierText(node.name);
|
|
946
|
+
return name ? `set ${name}` : void 0;
|
|
947
|
+
}
|
|
948
|
+
return void 0;
|
|
949
|
+
}
|
|
950
|
+
function constructorName(node) {
|
|
951
|
+
return ts4.isConstructorDeclaration(node) ? "constructor" : void 0;
|
|
952
|
+
}
|
|
953
|
+
function isNamedParent(node) {
|
|
954
|
+
return !!node && (ts4.isVariableDeclaration(node) || ts4.isPropertyAssignment(node) || ts4.isPropertyDeclaration(node));
|
|
955
|
+
}
|
|
956
|
+
function variableLikeFunctionName(node) {
|
|
957
|
+
return isNamedParent(node.parent) ? identifierText(node.parent.name) : void 0;
|
|
958
|
+
}
|
|
959
|
+
function getFunctionName(node) {
|
|
960
|
+
return declarationName(node) ?? accessorName(node) ?? constructorName(node) ?? (ts4.isArrowFunction(node) || ts4.isFunctionExpression(node) ? variableLikeFunctionName(node) : void 0) ?? "(anonymous)";
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
// src/crap/complexity/trackedFunctionNodes.ts
|
|
964
|
+
import * as ts5 from "typescript";
|
|
965
|
+
var TRACKED_FUNCTION_KINDS = /* @__PURE__ */ new Set([
|
|
966
|
+
ts5.SyntaxKind.FunctionDeclaration,
|
|
967
|
+
ts5.SyntaxKind.FunctionExpression,
|
|
968
|
+
ts5.SyntaxKind.ArrowFunction,
|
|
969
|
+
ts5.SyntaxKind.MethodDeclaration,
|
|
970
|
+
ts5.SyntaxKind.GetAccessor,
|
|
971
|
+
ts5.SyntaxKind.SetAccessor,
|
|
972
|
+
ts5.SyntaxKind.Constructor
|
|
973
|
+
]);
|
|
974
|
+
function isTrackedFunctionNode(node) {
|
|
975
|
+
return TRACKED_FUNCTION_KINDS.has(node.kind);
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
// src/crap/analysis/extractFunctions.ts
|
|
979
|
+
function extractFunctions(sourceFile) {
|
|
980
|
+
const functions = [];
|
|
981
|
+
function walk2(node) {
|
|
982
|
+
if (isTrackedFunctionNode(node)) {
|
|
983
|
+
const start = sourceFile.getLineAndCharacterOfPosition(node.getStart());
|
|
984
|
+
const end = sourceFile.getLineAndCharacterOfPosition(node.getEnd());
|
|
985
|
+
functions.push({
|
|
986
|
+
complexity: computeComplexity(node),
|
|
987
|
+
endLine: end.line + 1,
|
|
988
|
+
file: sourceFile.fileName,
|
|
989
|
+
line: start.line + 1,
|
|
990
|
+
name: getFunctionName(node)
|
|
991
|
+
});
|
|
992
|
+
}
|
|
993
|
+
ts6.forEachChild(node, walk2);
|
|
994
|
+
}
|
|
995
|
+
walk2(sourceFile);
|
|
996
|
+
return functions;
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
// src/crap/analysis/fileSelection.ts
|
|
1000
|
+
import { readFileSync as readFileSync4 } from "fs";
|
|
1001
|
+
import * as path from "path";
|
|
1002
|
+
import * as ts7 from "typescript";
|
|
1003
|
+
function matchesFilterScope(relativePath, filterScope) {
|
|
1004
|
+
if (!filterScope) {
|
|
1005
|
+
return true;
|
|
1006
|
+
}
|
|
1007
|
+
if (relativePath === filterScope) {
|
|
1008
|
+
return true;
|
|
1009
|
+
}
|
|
1010
|
+
return relativePath.startsWith(`${filterScope}/`);
|
|
1011
|
+
}
|
|
1012
|
+
function shouldIncludeFile(filePath, filterScope, repoRoot) {
|
|
1013
|
+
const relativePath = toPosix(path.relative(repoRoot, filePath));
|
|
1014
|
+
if (!matchesFilterScope(relativePath, filterScope)) {
|
|
1015
|
+
return false;
|
|
1016
|
+
}
|
|
1017
|
+
const workspacePackage = findContainingPackage(filePath, listWorkspacePackages(repoRoot));
|
|
1018
|
+
if (!workspacePackage) {
|
|
1019
|
+
return pathIncludedByDefaultTool(repoRoot, "crap", relativePath);
|
|
1020
|
+
}
|
|
1021
|
+
return pathIncludedByTool(
|
|
1022
|
+
repoRoot,
|
|
1023
|
+
workspacePackage.name,
|
|
1024
|
+
"crap",
|
|
1025
|
+
toPosix(path.relative(workspacePackage.root, filePath))
|
|
1026
|
+
);
|
|
1027
|
+
}
|
|
1028
|
+
function createSourceFile3(filePath) {
|
|
1029
|
+
return ts7.createSourceFile(
|
|
1030
|
+
filePath,
|
|
1031
|
+
readFileSync4(filePath, "utf-8"),
|
|
1032
|
+
ts7.ScriptTarget.Latest,
|
|
1033
|
+
true,
|
|
1034
|
+
filePath.endsWith(".tsx") ? ts7.ScriptKind.TSX : ts7.ScriptKind.TS
|
|
1035
|
+
);
|
|
1036
|
+
}
|
|
1037
|
+
|
|
1038
|
+
// src/crap/coverage/function.ts
|
|
1039
|
+
function statementsInRange(fn, fileCoverage) {
|
|
1040
|
+
return Object.entries(fileCoverage.statementMap).filter(([, location]) => location.start.line >= fn.line && location.end.line <= fn.endLine).map(([id]) => fileCoverage.s[id] > 0);
|
|
1041
|
+
}
|
|
1042
|
+
function getFunctionCoverage(fn, fileCoverage) {
|
|
1043
|
+
const statementCoverage = statementsInRange(fn, fileCoverage);
|
|
1044
|
+
if (statementCoverage.length === 0) {
|
|
1045
|
+
return 0;
|
|
1046
|
+
}
|
|
1047
|
+
const covered = statementCoverage.filter(Boolean).length;
|
|
1048
|
+
return covered / statementCoverage.length * 100;
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
// src/crap/analysis/run.ts
|
|
1052
|
+
function analyzeCoverageEntry(filePath, fileCoverage, repoRoot, threshold) {
|
|
1053
|
+
const sourceFile = createSourceFile3(filePath);
|
|
1054
|
+
return extractFunctions(sourceFile).map((fn) => {
|
|
1055
|
+
const coverage = getFunctionCoverage(fn, fileCoverage);
|
|
1056
|
+
const crap = calculateCrap(fn.complexity, coverage);
|
|
1057
|
+
return {
|
|
1058
|
+
complexity: fn.complexity,
|
|
1059
|
+
coverage: Math.round(coverage),
|
|
1060
|
+
crap: Math.round(crap * 100) / 100,
|
|
1061
|
+
file: toPosix(path2.relative(repoRoot, fn.file)),
|
|
1062
|
+
line: fn.line,
|
|
1063
|
+
name: fn.name
|
|
1064
|
+
};
|
|
1065
|
+
}).filter((result) => result.crap > threshold);
|
|
1066
|
+
}
|
|
1067
|
+
function analyzeCrap(coverageReports, repoRoot, filterScope, threshold = 8) {
|
|
1068
|
+
return coverageReports.flatMap((coverageReport) => Object.entries(coverageReport)).filter(([filePath]) => shouldIncludeFile(filePath, filterScope, repoRoot)).filter(([filePath]) => existsSync6(filePath)).flatMap(([filePath, fileCoverage]) => analyzeCoverageEntry(filePath, fileCoverage, repoRoot, threshold)).sort((left, right) => right.crap - left.crap);
|
|
1069
|
+
}
|
|
1070
|
+
|
|
1071
|
+
// src/crap/coverage/factories.ts
|
|
1072
|
+
import { isAbsolute as isAbsolute3, join as join9, relative as relative5, resolve as resolve7 } from "path";
|
|
1073
|
+
|
|
1074
|
+
// src/shared/util/reportKey.ts
|
|
1075
|
+
function trimEdgeDashes(value) {
|
|
1076
|
+
return value.replace(/^-+/, "").replace(/-+$/, "");
|
|
1077
|
+
}
|
|
1078
|
+
function reportKeySegments(value) {
|
|
1079
|
+
return value.toLowerCase().split(/[^a-z0-9.-]+/).map(trimEdgeDashes).filter((segment) => segment !== "");
|
|
1080
|
+
}
|
|
1081
|
+
function sanitizeReportKey(value) {
|
|
1082
|
+
return reportKeySegments(value).join("-");
|
|
1083
|
+
}
|
|
1084
|
+
|
|
1085
|
+
// src/crap/coverage/factories.ts
|
|
1086
|
+
function workspacePackageName(repoRoot, packageName) {
|
|
1087
|
+
if (!packageName) {
|
|
1088
|
+
return void 0;
|
|
1089
|
+
}
|
|
1090
|
+
const workspacePackage = listWorkspacePackages(repoRoot).find((entry) => entry.name === packageName);
|
|
1091
|
+
return workspacePackage?.manifestName ?? packageName;
|
|
1092
|
+
}
|
|
1093
|
+
function targetPackageRoot(repoRoot, target) {
|
|
1094
|
+
return target.packageRoot ?? repoRoot;
|
|
1095
|
+
}
|
|
1096
|
+
function reportKeyForTarget(target) {
|
|
1097
|
+
if (target.kind === "repo") {
|
|
1098
|
+
return "repo";
|
|
1099
|
+
}
|
|
1100
|
+
return target.packageName ?? sanitizeReportKey(target.relativePath);
|
|
1101
|
+
}
|
|
1102
|
+
function templateValues(repoRoot, target) {
|
|
1103
|
+
const packageName = target.packageName ?? "";
|
|
1104
|
+
const packageRoot = targetPackageRoot(repoRoot, target);
|
|
1105
|
+
return {
|
|
1106
|
+
packageJsonName: workspacePackageName(repoRoot, target.packageName) ?? packageName,
|
|
1107
|
+
packageName,
|
|
1108
|
+
packageRoot,
|
|
1109
|
+
reportKey: reportKeyForTarget(target),
|
|
1110
|
+
reportsDir: relativeReportsDir(repoRoot),
|
|
1111
|
+
repoRoot,
|
|
1112
|
+
target: target.relativePath,
|
|
1113
|
+
targetPath: target.relativePath
|
|
1114
|
+
};
|
|
1115
|
+
}
|
|
1116
|
+
function applyTemplate(value, values) {
|
|
1117
|
+
return value.replace(/\{([a-zA-Z]+)\}/g, (match, rawKey) => {
|
|
1118
|
+
const key = rawKey;
|
|
1119
|
+
return values[key] ?? match;
|
|
1120
|
+
});
|
|
1121
|
+
}
|
|
1122
|
+
function resolvePathFromRepo(repoRoot, value) {
|
|
1123
|
+
return isAbsolute3(value) ? value : resolve7(repoRoot, value);
|
|
1124
|
+
}
|
|
1125
|
+
function configuredCoverageProfile(repoRoot, target, config) {
|
|
1126
|
+
const values = templateValues(repoRoot, target);
|
|
1127
|
+
const cwd = config.cwd ? resolvePathFromRepo(repoRoot, applyTemplate(config.cwd, values)) : repoRoot;
|
|
1128
|
+
const defaultProfile = defaultCoverageProfile(repoRoot, target);
|
|
1129
|
+
const coveragePath = config.coveragePath ? resolvePathFromRepo(repoRoot, applyTemplate(config.coveragePath, values)) : defaultProfile.coveragePath;
|
|
1130
|
+
return {
|
|
1131
|
+
args: (config.args ?? defaultProfile.args).map((arg) => applyTemplate(arg, values)),
|
|
1132
|
+
command: config.command ? applyTemplate(config.command, values) : defaultProfile.command,
|
|
1133
|
+
coveragePath,
|
|
1134
|
+
cwd,
|
|
1135
|
+
...config.env ? {
|
|
1136
|
+
env: Object.fromEntries(
|
|
1137
|
+
Object.entries(config.env).map(([key, value]) => [key, applyTemplate(value, values)])
|
|
1138
|
+
)
|
|
1139
|
+
} : {}
|
|
1140
|
+
};
|
|
1141
|
+
}
|
|
1142
|
+
function defaultCoverageProfile(repoRoot, target) {
|
|
1143
|
+
const packageJsonName = workspacePackageName(repoRoot, target.packageName);
|
|
1144
|
+
const reportDirectory2 = resolveReportPath(repoRoot, "crap", reportKeyForTarget(target));
|
|
1145
|
+
if (target.packageName && packageJsonName) {
|
|
1146
|
+
return {
|
|
1147
|
+
args: [
|
|
1148
|
+
"--filter",
|
|
1149
|
+
packageJsonName,
|
|
1150
|
+
"exec",
|
|
1151
|
+
"vitest",
|
|
1152
|
+
"run",
|
|
1153
|
+
"--coverage",
|
|
1154
|
+
"--coverage.reportsDirectory",
|
|
1155
|
+
reportDirectory2
|
|
1156
|
+
],
|
|
1157
|
+
command: "pnpm",
|
|
1158
|
+
coveragePath: join9(reportDirectory2, "coverage-final.json"),
|
|
1159
|
+
cwd: repoRoot
|
|
1160
|
+
};
|
|
1161
|
+
}
|
|
1162
|
+
return {
|
|
1163
|
+
args: ["exec", "vitest", "run", "--coverage", "--coverage.reportsDirectory", reportDirectory2],
|
|
1164
|
+
command: "pnpm",
|
|
1165
|
+
coveragePath: join9(reportDirectory2, "coverage-final.json"),
|
|
1166
|
+
cwd: repoRoot
|
|
1167
|
+
};
|
|
1168
|
+
}
|
|
1169
|
+
function coverageProfilesForTarget(repoRoot, target) {
|
|
1170
|
+
const configuredProfiles = resolvePackageCrapCoverage(repoRoot, target.packageName);
|
|
1171
|
+
if (configuredProfiles.length > 0) {
|
|
1172
|
+
return configuredProfiles.map((config) => configuredCoverageProfile(repoRoot, target, config));
|
|
1173
|
+
}
|
|
1174
|
+
return [defaultCoverageProfile(repoRoot, target)];
|
|
1175
|
+
}
|
|
1176
|
+
|
|
1177
|
+
// src/crap/coverage/profiles.ts
|
|
1178
|
+
function createCoverageProfiles(repoRoot, target) {
|
|
1179
|
+
return coverageProfilesForTarget(repoRoot, target);
|
|
1180
|
+
}
|
|
1181
|
+
|
|
1182
|
+
// src/crap/coverage/read.ts
|
|
1183
|
+
import { existsSync as existsSync7, readFileSync as readFileSync5 } from "fs";
|
|
1184
|
+
function readCoverageReport(path3) {
|
|
1185
|
+
if (!existsSync7(path3)) {
|
|
1186
|
+
throw new Error(`Coverage data not found: ${path3}`);
|
|
1187
|
+
}
|
|
1188
|
+
return JSON.parse(readFileSync5(path3, "utf-8"));
|
|
1189
|
+
}
|
|
1190
|
+
|
|
1191
|
+
// src/crap/report.ts
|
|
1192
|
+
function reportCrap(results, threshold) {
|
|
1193
|
+
if (results.length === 0) {
|
|
1194
|
+
console.log(`
|
|
1195
|
+
\u2705 All functions have CRAP score \u2264 ${threshold}.
|
|
1196
|
+
`);
|
|
1197
|
+
return;
|
|
1198
|
+
}
|
|
1199
|
+
console.log(`
|
|
1200
|
+
\u26A0\uFE0F CRAP SCORE THRESHOLD EXCEEDED (max: ${threshold})`);
|
|
1201
|
+
console.log("\u2501".repeat(70));
|
|
1202
|
+
console.log("Functions with high complexity and low test coverage.\n");
|
|
1203
|
+
console.log(`${"CRAP".padStart(6)} ${"Comp".padStart(4)} ${"Cov%".padStart(4)} Function`);
|
|
1204
|
+
console.log(`${"\u2500".repeat(6)} ${"\u2500".repeat(4)} ${"\u2500".repeat(4)} ${"\u2500".repeat(50)}`);
|
|
1205
|
+
for (const result of results) {
|
|
1206
|
+
console.log(
|
|
1207
|
+
`${result.crap.toFixed(1).padStart(6)} ${String(result.complexity).padStart(4)} ${`${result.coverage}%`.padStart(4)} ${result.name} (${result.file}:${result.line})`
|
|
1208
|
+
);
|
|
1209
|
+
}
|
|
1210
|
+
console.log(`
|
|
1211
|
+
${"\u2501".repeat(70)}`);
|
|
1212
|
+
console.log(`${results.length} function(s) exceed CRAP threshold of ${threshold}.`);
|
|
1213
|
+
console.log("Refactor to reduce complexity or add tests to increase coverage.\n");
|
|
1214
|
+
}
|
|
1215
|
+
|
|
1216
|
+
// src/shared/scope/source.ts
|
|
1217
|
+
function resolveSourceScope(target) {
|
|
1218
|
+
if (target.kind === "repo") {
|
|
1219
|
+
return void 0;
|
|
1220
|
+
}
|
|
1221
|
+
return target.relativePath;
|
|
1222
|
+
}
|
|
1223
|
+
function assertSourceScope(target) {
|
|
1224
|
+
const scope = resolveSourceScope(target);
|
|
1225
|
+
if (!scope && target.kind !== "repo") {
|
|
1226
|
+
throw new Error(
|
|
1227
|
+
"This command expects the repo root, a package root, a directory, or a file target."
|
|
1228
|
+
);
|
|
1229
|
+
}
|
|
1230
|
+
return scope;
|
|
1231
|
+
}
|
|
1232
|
+
|
|
1233
|
+
// src/shared/runCommand.ts
|
|
1234
|
+
import * as childProcess from "child_process";
|
|
1235
|
+
function runCommand(command2, args2, cwd, env) {
|
|
1236
|
+
childProcess.execFileSync(command2, args2, {
|
|
1237
|
+
cwd,
|
|
1238
|
+
env: env ? { ...process.env, ...env } : process.env,
|
|
1239
|
+
stdio: "inherit"
|
|
1240
|
+
});
|
|
1241
|
+
}
|
|
1242
|
+
|
|
1243
|
+
// src/crap/command.ts
|
|
1244
|
+
var DEFAULT_DEPENDENCIES2 = {
|
|
1245
|
+
analyzeCrap,
|
|
1246
|
+
createCoverageProfiles,
|
|
1247
|
+
readCoverageReport,
|
|
1248
|
+
reportCrap,
|
|
1249
|
+
resolveQualityTarget,
|
|
1250
|
+
runCommand
|
|
1251
|
+
};
|
|
1252
|
+
function parseThreshold(args2) {
|
|
1253
|
+
const rawThreshold = flagValue(args2, "--threshold");
|
|
1254
|
+
if (rawThreshold === void 0) {
|
|
1255
|
+
return 8;
|
|
1256
|
+
}
|
|
1257
|
+
const threshold = Number(rawThreshold);
|
|
1258
|
+
if (rawThreshold.trim() === "" || !Number.isFinite(threshold)) {
|
|
1259
|
+
throw new Error(`Invalid CRAP threshold: ${rawThreshold}`);
|
|
1260
|
+
}
|
|
1261
|
+
return threshold;
|
|
1262
|
+
}
|
|
1263
|
+
function runCrapCli(rawArgs, dependencies = DEFAULT_DEPENDENCIES2) {
|
|
1264
|
+
const args2 = cleanCliArgs(rawArgs);
|
|
1265
|
+
const target = dependencies.resolveQualityTarget(REPO_ROOT, parseTargetArg(args2, ["--threshold"]));
|
|
1266
|
+
const threshold = parseThreshold(args2);
|
|
1267
|
+
const filterScope = assertSourceScope(target);
|
|
1268
|
+
const profiles = dependencies.createCoverageProfiles(REPO_ROOT, target);
|
|
1269
|
+
profiles.forEach((profile) => {
|
|
1270
|
+
if (profile.env) {
|
|
1271
|
+
dependencies.runCommand(profile.command, profile.args, profile.cwd, profile.env);
|
|
1272
|
+
return;
|
|
1273
|
+
}
|
|
1274
|
+
dependencies.runCommand(profile.command, profile.args, profile.cwd);
|
|
1275
|
+
});
|
|
1276
|
+
const reports = profiles.map((profile) => dependencies.readCoverageReport(profile.coveragePath));
|
|
1277
|
+
const results = dependencies.analyzeCrap(reports, REPO_ROOT, filterScope, threshold);
|
|
1278
|
+
dependencies.reportCrap(results, threshold);
|
|
1279
|
+
}
|
|
1280
|
+
|
|
1281
|
+
// src/cli/init.ts
|
|
1282
|
+
import { existsSync as existsSync8, writeFileSync } from "node:fs";
|
|
1283
|
+
import { join as join10 } from "node:path";
|
|
1284
|
+
var CONFIG_FILE2 = "quality.config.json";
|
|
1285
|
+
var DEFAULT_CONFIG = {
|
|
1286
|
+
reportsDir: "reports/quality-tools",
|
|
1287
|
+
defaults: {
|
|
1288
|
+
mutation: {
|
|
1289
|
+
include: ["src/**/*.ts", "src/**/*.tsx"],
|
|
1290
|
+
exclude: ["src/**/*.d.ts"]
|
|
1291
|
+
},
|
|
1292
|
+
crap: {
|
|
1293
|
+
coverage: {
|
|
1294
|
+
command: "pnpm",
|
|
1295
|
+
args: [
|
|
1296
|
+
"exec",
|
|
1297
|
+
"vitest",
|
|
1298
|
+
"run",
|
|
1299
|
+
"--coverage",
|
|
1300
|
+
"--coverage.reportsDirectory",
|
|
1301
|
+
"{repoRoot}/{reportsDir}/crap/{reportKey}"
|
|
1302
|
+
],
|
|
1303
|
+
coveragePath: "{repoRoot}/{reportsDir}/crap/{reportKey}/coverage-final.json"
|
|
1304
|
+
},
|
|
1305
|
+
exclude: ["**/*.test.ts", "**/*.test.tsx", "**/*.d.ts"]
|
|
1306
|
+
},
|
|
1307
|
+
scrap: {
|
|
1308
|
+
include: ["tests/**/*.test.ts", "tests/**/*.test.tsx", "__tests__/**/*.test.ts", "__tests__/**/*.test.tsx"],
|
|
1309
|
+
exclude: []
|
|
1310
|
+
},
|
|
1311
|
+
boundaries: {
|
|
1312
|
+
include: ["src/**/*.ts", "src/**/*.tsx"],
|
|
1313
|
+
exclude: ["src/**/*.d.ts", "**/*.test.ts", "**/*.test.tsx"]
|
|
1314
|
+
},
|
|
1315
|
+
organize: {
|
|
1316
|
+
lowInfoNames: {
|
|
1317
|
+
banned: ["utils", "helpers", "misc", "common", "shared", "_shared", "lib", "index"],
|
|
1318
|
+
discouraged: ["types", "constants", "config", "base", "core"]
|
|
1319
|
+
}
|
|
1320
|
+
}
|
|
1321
|
+
},
|
|
1322
|
+
packages: {}
|
|
1323
|
+
};
|
|
1324
|
+
function runInitCli(_args = [], cwd = process.cwd()) {
|
|
1325
|
+
const configPath = join10(cwd, CONFIG_FILE2);
|
|
1326
|
+
if (existsSync8(configPath)) {
|
|
1327
|
+
console.log(`${CONFIG_FILE2} already exists`);
|
|
1328
|
+
return;
|
|
1329
|
+
}
|
|
1330
|
+
writeFileSync(configPath, `${JSON.stringify(DEFAULT_CONFIG, null, 2)}
|
|
1331
|
+
`);
|
|
1332
|
+
console.log(`Created ${CONFIG_FILE2}`);
|
|
1333
|
+
}
|
|
1334
|
+
|
|
1335
|
+
// src/mutation/runner/run.ts
|
|
1336
|
+
import { spawn } from "child_process";
|
|
1337
|
+
|
|
1338
|
+
// src/mutation/reporting/reportArtifacts.ts
|
|
1339
|
+
import { cpSync, existsSync as existsSync9, mkdirSync } from "fs";
|
|
1340
|
+
import { join as join11 } from "path";
|
|
1341
|
+
function rootReportDirectory(repoRoot = REPO_ROOT) {
|
|
1342
|
+
return relativeReportPath(repoRoot, "mutation");
|
|
1343
|
+
}
|
|
1344
|
+
function reportDirectory(reportKey, repoRoot = REPO_ROOT) {
|
|
1345
|
+
return `${rootReportDirectory(repoRoot)}/${reportKey}`;
|
|
1346
|
+
}
|
|
1347
|
+
function incrementalReportPath(reportKey, repoRoot = REPO_ROOT) {
|
|
1348
|
+
return `${reportDirectory(reportKey, repoRoot)}/stryker-incremental-${reportKey}.json`;
|
|
1349
|
+
}
|
|
1350
|
+
function copySharedMutationReports(reportKey, repoRoot = process.cwd()) {
|
|
1351
|
+
const targetDirectory = join11(repoRoot, reportDirectory(reportKey, repoRoot));
|
|
1352
|
+
mkdirSync(targetDirectory, { recursive: true });
|
|
1353
|
+
const sharedJson = join11(repoRoot, rootReportDirectory(repoRoot), "mutation.json");
|
|
1354
|
+
const sharedHtml = join11(repoRoot, rootReportDirectory(repoRoot), "mutation.html");
|
|
1355
|
+
const targetIncremental = join11(repoRoot, incrementalReportPath(reportKey, repoRoot));
|
|
1356
|
+
if (existsSync9(sharedJson)) {
|
|
1357
|
+
cpSync(sharedJson, `${targetDirectory}/mutation.json`);
|
|
1358
|
+
}
|
|
1359
|
+
if (existsSync9(sharedHtml)) {
|
|
1360
|
+
cpSync(sharedHtml, `${targetDirectory}/mutation.html`);
|
|
1361
|
+
}
|
|
1362
|
+
if (!existsSync9(targetIncremental) && existsSync9(sharedJson)) {
|
|
1363
|
+
cpSync(sharedJson, targetIncremental);
|
|
1364
|
+
}
|
|
1365
|
+
return join11(targetDirectory, "mutation.json");
|
|
1366
|
+
}
|
|
1367
|
+
|
|
1368
|
+
// src/mutation/reporting/check.ts
|
|
1369
|
+
import { readFileSync as readFileSync6 } from "fs";
|
|
1370
|
+
function findMutationSiteViolations(reportPath, threshold = 50) {
|
|
1371
|
+
const report = JSON.parse(readFileSync6(reportPath, "utf-8"));
|
|
1372
|
+
return Object.entries(report.files ?? {}).map(([file, entry]) => ({ count: (entry.mutants ?? []).length, file })).filter((entry) => entry.count > threshold).sort((left, right) => right.count - left.count);
|
|
1373
|
+
}
|
|
1374
|
+
function reportMutationSiteViolations(reportPath, threshold = 50) {
|
|
1375
|
+
const violations = findMutationSiteViolations(reportPath, threshold);
|
|
1376
|
+
if (violations.length === 0) {
|
|
1377
|
+
console.log(`
|
|
1378
|
+
\u2705 All files are within the mutation site threshold (${threshold}).
|
|
1379
|
+
`);
|
|
1380
|
+
return;
|
|
1381
|
+
}
|
|
1382
|
+
console.log(`
|
|
1383
|
+
\u26A0\uFE0F MUTATION SITE THRESHOLD EXCEEDED (max: ${threshold})`);
|
|
1384
|
+
console.log("\u2501".repeat(60));
|
|
1385
|
+
console.log("The following files have too many mutation sites, indicating");
|
|
1386
|
+
console.log("high complexity. Consider splitting them into smaller modules.\n");
|
|
1387
|
+
for (const violation of violations) {
|
|
1388
|
+
console.log(` ${violation.count} mutation sites \u2192 ${violation.file}`);
|
|
1389
|
+
}
|
|
1390
|
+
console.log(`
|
|
1391
|
+
${"\u2501".repeat(60)}`);
|
|
1392
|
+
console.log(`${violations.length} file(s) exceed the threshold of ${threshold} mutation sites.
|
|
1393
|
+
`);
|
|
1394
|
+
}
|
|
1395
|
+
|
|
1396
|
+
// src/mutation/analysis/mutateGlobs.ts
|
|
1397
|
+
function buildScopeIncludes(scope, kind) {
|
|
1398
|
+
if (kind === "file") {
|
|
1399
|
+
return [scope];
|
|
1400
|
+
}
|
|
1401
|
+
return [`${scope}/**/*.ts`, `${scope}/**/*.tsx`];
|
|
1402
|
+
}
|
|
1403
|
+
function buildMutateGlobs(target, patterns) {
|
|
1404
|
+
if (target.kind === "repo") {
|
|
1405
|
+
return [
|
|
1406
|
+
...patterns.include.length > 0 ? patterns.include : ["**/*.ts", "**/*.tsx"],
|
|
1407
|
+
...patterns.exclude.map((pattern) => `!${pattern}`)
|
|
1408
|
+
];
|
|
1409
|
+
}
|
|
1410
|
+
if (target.kind === "package") {
|
|
1411
|
+
return [
|
|
1412
|
+
...patterns.include.length > 0 ? patterns.include : [`${target.relativePath}/**/*.ts`, `${target.relativePath}/**/*.tsx`],
|
|
1413
|
+
...patterns.exclude.map((pattern) => `!${pattern}`)
|
|
1414
|
+
];
|
|
1415
|
+
}
|
|
1416
|
+
return [
|
|
1417
|
+
...buildScopeIncludes(target.relativePath, target.kind),
|
|
1418
|
+
...patterns.exclude.map((pattern) => `!${pattern}`)
|
|
1419
|
+
];
|
|
1420
|
+
}
|
|
1421
|
+
|
|
1422
|
+
// src/mutation/analysis/profile.ts
|
|
1423
|
+
import { existsSync as existsSync10 } from "fs";
|
|
1424
|
+
import { join as join12 } from "path";
|
|
1425
|
+
function defaultHostStrykerConfig(repoRoot) {
|
|
1426
|
+
return [
|
|
1427
|
+
"stryker.config.cjs",
|
|
1428
|
+
"stryker.config.mjs",
|
|
1429
|
+
"stryker.config.js",
|
|
1430
|
+
"stryker.conf.js"
|
|
1431
|
+
].map((fileName) => join12(repoRoot, fileName)).find((configPath) => existsSync10(configPath));
|
|
1432
|
+
}
|
|
1433
|
+
function resolveMutationProfile(target) {
|
|
1434
|
+
const packageConfig = resolveMutationStrykerConfig(REPO_ROOT, target.packageName) ?? defaultHostStrykerConfig(REPO_ROOT) ?? `${PACKAGE_ROOT}/stryker.config.cjs`;
|
|
1435
|
+
return {
|
|
1436
|
+
configPath: packageConfig,
|
|
1437
|
+
...target.packageName ? { packageName: target.packageName } : {}
|
|
1438
|
+
};
|
|
1439
|
+
}
|
|
1440
|
+
|
|
1441
|
+
// src/mutation/runner/args.ts
|
|
1442
|
+
function configuredExcludeGlobs(patterns) {
|
|
1443
|
+
return patterns.exclude.map((pattern) => `!${pattern}`);
|
|
1444
|
+
}
|
|
1445
|
+
function buildMutationArgs(target, options = {}) {
|
|
1446
|
+
const profile = resolveMutationProfile(target);
|
|
1447
|
+
const reportKey = target.kind === "repo" ? "repo" : target.kind === "package" && profile.packageName ? profile.packageName : sanitizeReportKey(target.relativePath);
|
|
1448
|
+
const args2 = ["run", profile.configPath, "--incrementalFile", incrementalReportPath(reportKey)];
|
|
1449
|
+
if (options.force) {
|
|
1450
|
+
args2.push("--force");
|
|
1451
|
+
}
|
|
1452
|
+
const configPatterns = profile.packageName ? resolvePackageToolGlobs(REPO_ROOT, profile.packageName, "mutation") : resolveDefaultToolPatterns(REPO_ROOT, "mutation");
|
|
1453
|
+
const mutateGlobs = options.mutateGlobs ? [...options.mutateGlobs, ...configuredExcludeGlobs(configPatterns)] : buildMutateGlobs(target, configPatterns);
|
|
1454
|
+
args2.push("-m", mutateGlobs.join(","));
|
|
1455
|
+
return { args: args2, reportKey };
|
|
1456
|
+
}
|
|
1457
|
+
|
|
1458
|
+
// src/mutation/runner/strykerBinary.ts
|
|
1459
|
+
import { createRequire } from "node:module";
|
|
1460
|
+
import { dirname as dirname6, join as join13 } from "node:path";
|
|
1461
|
+
var require2 = createRequire(import.meta.url);
|
|
1462
|
+
function strykerBinPath() {
|
|
1463
|
+
return join13(dirname6(require2.resolve("@stryker-mutator/core/package.json")), "bin/stryker.js");
|
|
1464
|
+
}
|
|
1465
|
+
|
|
1466
|
+
// src/mutation/runner/environment.ts
|
|
1467
|
+
function buildMutationEnv(options = {}) {
|
|
1468
|
+
return {
|
|
1469
|
+
...process.env,
|
|
1470
|
+
QUALITY_TOOLS_REPORTS_DIR: relativeReportsDir(REPO_ROOT),
|
|
1471
|
+
...options.testIncludes ? { QUALITY_TOOLS_VITEST_INCLUDE_JSON: JSON.stringify(options.testIncludes) } : {}
|
|
1472
|
+
};
|
|
1473
|
+
}
|
|
1474
|
+
|
|
1475
|
+
// src/mutation/runner/run.ts
|
|
1476
|
+
var MUTATION_PROGRESS_INTERVAL_MS = 6e4;
|
|
1477
|
+
function formatElapsedDuration(durationMs) {
|
|
1478
|
+
const totalSeconds = Math.max(0, Math.floor(durationMs / 1e3));
|
|
1479
|
+
const minutes = Math.floor(totalSeconds / 60);
|
|
1480
|
+
const seconds = totalSeconds % 60;
|
|
1481
|
+
return minutes > 0 ? `${minutes}m ${seconds}s` : `${seconds}s`;
|
|
1482
|
+
}
|
|
1483
|
+
function runStryker(args2, env, target) {
|
|
1484
|
+
return new Promise((resolve8, reject) => {
|
|
1485
|
+
const startedAt = Date.now();
|
|
1486
|
+
const child = spawn(process.execPath, [strykerBinPath(), ...args2], {
|
|
1487
|
+
cwd: REPO_ROOT,
|
|
1488
|
+
env,
|
|
1489
|
+
stdio: "inherit"
|
|
1490
|
+
});
|
|
1491
|
+
const progressTimer = setInterval(() => {
|
|
1492
|
+
console.error(
|
|
1493
|
+
`[mutation] Still running ${target.relativePath} after ${formatElapsedDuration(Date.now() - startedAt)}...`
|
|
1494
|
+
);
|
|
1495
|
+
}, MUTATION_PROGRESS_INTERVAL_MS);
|
|
1496
|
+
const clearProgressTimer = () => {
|
|
1497
|
+
clearInterval(progressTimer);
|
|
1498
|
+
};
|
|
1499
|
+
child.once("error", (error) => {
|
|
1500
|
+
clearProgressTimer();
|
|
1501
|
+
reject(error);
|
|
1502
|
+
});
|
|
1503
|
+
child.once("exit", (code, signal) => {
|
|
1504
|
+
clearProgressTimer();
|
|
1505
|
+
if (code === 0) {
|
|
1506
|
+
resolve8();
|
|
1507
|
+
return;
|
|
1508
|
+
}
|
|
1509
|
+
reject(new Error(`Stryker exited with ${signal ? `signal ${signal}` : `code ${code ?? "unknown"}`}.`));
|
|
1510
|
+
});
|
|
1511
|
+
});
|
|
1512
|
+
}
|
|
1513
|
+
async function runMutation(target, options = {}) {
|
|
1514
|
+
const { args: args2, reportKey } = buildMutationArgs(target, options);
|
|
1515
|
+
await runStryker(args2, buildMutationEnv(options), target);
|
|
1516
|
+
const reportPath = copySharedMutationReports(reportKey, REPO_ROOT);
|
|
1517
|
+
reportMutationSiteViolations(reportPath);
|
|
1518
|
+
}
|
|
1519
|
+
|
|
1520
|
+
// src/mutation/runner/command.ts
|
|
1521
|
+
var VALUE_FLAGS = /* @__PURE__ */ new Set([
|
|
1522
|
+
"--mutate",
|
|
1523
|
+
"--mutate-glob",
|
|
1524
|
+
"--mutate-globs-json",
|
|
1525
|
+
"--test-include",
|
|
1526
|
+
"--test-includes-json"
|
|
1527
|
+
]);
|
|
1528
|
+
function createDefaultMutationCliDependencies() {
|
|
1529
|
+
return {
|
|
1530
|
+
resolveQualityTarget,
|
|
1531
|
+
runMutation
|
|
1532
|
+
};
|
|
1533
|
+
}
|
|
1534
|
+
function resolveCliTargets(input, mutateInput, dependencies) {
|
|
1535
|
+
if (mutateInput) {
|
|
1536
|
+
return [dependencies.resolveQualityTarget(REPO_ROOT, mutateInput)];
|
|
1537
|
+
}
|
|
1538
|
+
if (input) {
|
|
1539
|
+
return [dependencies.resolveQualityTarget(REPO_ROOT, input)];
|
|
1540
|
+
}
|
|
1541
|
+
throw new Error(
|
|
1542
|
+
"Mutation requires an explicit package, directory, file, or repo target. Example: `quality-tools mutate .` or `quality-tools mutate packages/foo/src/bar.ts`."
|
|
1543
|
+
);
|
|
1544
|
+
}
|
|
1545
|
+
function parseBareMutationTargetArg(args2) {
|
|
1546
|
+
for (let index = 0; index < args2.length; index += 1) {
|
|
1547
|
+
const arg = args2[index];
|
|
1548
|
+
if (VALUE_FLAGS.has(arg)) {
|
|
1549
|
+
index += 1;
|
|
1550
|
+
continue;
|
|
1551
|
+
}
|
|
1552
|
+
if (!arg.startsWith("--")) {
|
|
1553
|
+
return arg;
|
|
1554
|
+
}
|
|
1555
|
+
}
|
|
1556
|
+
return void 0;
|
|
1557
|
+
}
|
|
1558
|
+
function collectFlagValues(args2, name) {
|
|
1559
|
+
const values = [];
|
|
1560
|
+
for (let index = 0; index < args2.length; index += 1) {
|
|
1561
|
+
const arg = args2[index];
|
|
1562
|
+
if (arg === name && args2[index + 1]) {
|
|
1563
|
+
values.push(args2[index + 1]);
|
|
1564
|
+
index += 1;
|
|
1565
|
+
continue;
|
|
1566
|
+
}
|
|
1567
|
+
if (arg.startsWith(`${name}=`)) {
|
|
1568
|
+
values.push(arg.slice(name.length + 1));
|
|
1569
|
+
}
|
|
1570
|
+
}
|
|
1571
|
+
return values;
|
|
1572
|
+
}
|
|
1573
|
+
function parseJsonStringArray(value, flagName) {
|
|
1574
|
+
if (!value) {
|
|
1575
|
+
return [];
|
|
1576
|
+
}
|
|
1577
|
+
const parsed = JSON.parse(value);
|
|
1578
|
+
if (!Array.isArray(parsed) || parsed.some((entry) => typeof entry !== "string")) {
|
|
1579
|
+
throw new Error(`${flagName} must be a JSON array of strings.`);
|
|
1580
|
+
}
|
|
1581
|
+
return parsed;
|
|
1582
|
+
}
|
|
1583
|
+
function mutationRunOptions(args2) {
|
|
1584
|
+
const mutateGlobs = [
|
|
1585
|
+
...collectFlagValues(args2, "--mutate-glob"),
|
|
1586
|
+
...parseJsonStringArray(flagValue(args2, "--mutate-globs-json"), "--mutate-globs-json")
|
|
1587
|
+
];
|
|
1588
|
+
const testIncludes = [
|
|
1589
|
+
...collectFlagValues(args2, "--test-include"),
|
|
1590
|
+
...parseJsonStringArray(flagValue(args2, "--test-includes-json"), "--test-includes-json")
|
|
1591
|
+
];
|
|
1592
|
+
return {
|
|
1593
|
+
force: args2.includes("--force"),
|
|
1594
|
+
...mutateGlobs.length > 0 ? { mutateGlobs } : {},
|
|
1595
|
+
...testIncludes.length > 0 ? { testIncludes } : {}
|
|
1596
|
+
};
|
|
1597
|
+
}
|
|
1598
|
+
async function runMutationCli(rawArgs, dependencies = createDefaultMutationCliDependencies()) {
|
|
1599
|
+
const args2 = cleanCliArgs(rawArgs);
|
|
1600
|
+
const targets = resolveCliTargets(
|
|
1601
|
+
parseBareMutationTargetArg(args2),
|
|
1602
|
+
flagValue(args2, "--mutate"),
|
|
1603
|
+
dependencies
|
|
1604
|
+
);
|
|
1605
|
+
const options = mutationRunOptions(args2);
|
|
1606
|
+
for (const target of targets) {
|
|
1607
|
+
await dependencies.runMutation(target, options);
|
|
1608
|
+
}
|
|
1609
|
+
}
|
|
1610
|
+
|
|
1611
|
+
// src/organize/command.ts
|
|
1612
|
+
import { mkdirSync as mkdirSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
1613
|
+
import { join as join16 } from "path";
|
|
1614
|
+
|
|
1615
|
+
// src/organize/analyze/run.ts
|
|
1616
|
+
import { relative as relative7 } from "path";
|
|
1617
|
+
|
|
1618
|
+
// src/organize/rules.ts
|
|
1619
|
+
import { readFileSync as readFileSync7 } from "fs";
|
|
1620
|
+
import { join as join14 } from "path";
|
|
1621
|
+
var DEFAULT_CONFIG2 = {
|
|
1622
|
+
lowInfoNames: {
|
|
1623
|
+
banned: ["utils", "helpers", "misc", "common", "shared", "_shared", "lib", "index"],
|
|
1624
|
+
discouraged: ["types", "constants", "config", "base", "core"]
|
|
1625
|
+
},
|
|
1626
|
+
fileFanOut: { warning: 8, split: 10 },
|
|
1627
|
+
folderFanOut: { warning: 10, split: 13 },
|
|
1628
|
+
depth: { warning: 4, deep: 5 },
|
|
1629
|
+
redundancyThreshold: 0.3,
|
|
1630
|
+
cohesionClusterMinSize: 3
|
|
1631
|
+
};
|
|
1632
|
+
var CONFIG_FILE3 = "quality.config.json";
|
|
1633
|
+
function mergeConfig(defaults, overrides) {
|
|
1634
|
+
return {
|
|
1635
|
+
lowInfoNames: overrides.lowInfoNames ?? defaults.lowInfoNames,
|
|
1636
|
+
fileFanOut: overrides.fileFanOut ?? defaults.fileFanOut,
|
|
1637
|
+
folderFanOut: overrides.folderFanOut ?? defaults.folderFanOut,
|
|
1638
|
+
depth: overrides.depth ?? defaults.depth,
|
|
1639
|
+
redundancyThreshold: overrides.redundancyThreshold ?? defaults.redundancyThreshold,
|
|
1640
|
+
cohesionClusterMinSize: overrides.cohesionClusterMinSize ?? defaults.cohesionClusterMinSize
|
|
1641
|
+
};
|
|
1642
|
+
}
|
|
1643
|
+
function loadOrganizeConfig(repoRoot, packageName) {
|
|
1644
|
+
const configPath = join14(repoRoot, CONFIG_FILE3);
|
|
1645
|
+
try {
|
|
1646
|
+
const rawConfig = JSON.parse(readFileSync7(configPath, "utf-8"));
|
|
1647
|
+
const defaultConfig = rawConfig.defaults?.organize;
|
|
1648
|
+
const packageConfig = packageName ? rawConfig.packages?.[packageName]?.organize : void 0;
|
|
1649
|
+
const mergedDefaults = defaultConfig ? mergeConfig(DEFAULT_CONFIG2, defaultConfig) : DEFAULT_CONFIG2;
|
|
1650
|
+
return packageConfig ? mergeConfig(mergedDefaults, packageConfig) : mergedDefaults;
|
|
1651
|
+
} catch {
|
|
1652
|
+
return DEFAULT_CONFIG2;
|
|
1653
|
+
}
|
|
1654
|
+
}
|
|
1655
|
+
|
|
1656
|
+
// src/organize/metric/fileFanOut.ts
|
|
1657
|
+
function fileFanOutVerdict(fileCount, warningThreshold, splitThreshold) {
|
|
1658
|
+
if (fileCount >= splitThreshold) {
|
|
1659
|
+
return "SPLIT";
|
|
1660
|
+
}
|
|
1661
|
+
if (fileCount >= warningThreshold) {
|
|
1662
|
+
return "WARNING";
|
|
1663
|
+
}
|
|
1664
|
+
return "STABLE";
|
|
1665
|
+
}
|
|
1666
|
+
|
|
1667
|
+
// src/organize/metric/folderFanOut.ts
|
|
1668
|
+
function folderFanOutVerdict(folderCount, warningThreshold, splitThreshold) {
|
|
1669
|
+
if (folderCount >= splitThreshold) {
|
|
1670
|
+
return "SPLIT";
|
|
1671
|
+
}
|
|
1672
|
+
if (folderCount >= warningThreshold) {
|
|
1673
|
+
return "WARNING";
|
|
1674
|
+
}
|
|
1675
|
+
return "STABLE";
|
|
1676
|
+
}
|
|
1677
|
+
|
|
1678
|
+
// src/organize/metric/directoryDepth.ts
|
|
1679
|
+
import { relative as relative6, sep as sep2 } from "path";
|
|
1680
|
+
function directoryDepth(directoryPath, targetRoot) {
|
|
1681
|
+
const relativePath = relative6(targetRoot, directoryPath);
|
|
1682
|
+
if (relativePath === "") {
|
|
1683
|
+
return 0;
|
|
1684
|
+
}
|
|
1685
|
+
return relativePath.split(sep2).length;
|
|
1686
|
+
}
|
|
1687
|
+
function depthVerdict(depth, warningThreshold, deepThreshold) {
|
|
1688
|
+
if (depth >= deepThreshold) {
|
|
1689
|
+
return "DEEP";
|
|
1690
|
+
}
|
|
1691
|
+
if (depth >= warningThreshold) {
|
|
1692
|
+
return "WARNING";
|
|
1693
|
+
}
|
|
1694
|
+
return "STABLE";
|
|
1695
|
+
}
|
|
1696
|
+
|
|
1697
|
+
// src/organize/cohesion/imports/graph.ts
|
|
1698
|
+
import { join as join15 } from "path";
|
|
1699
|
+
|
|
1700
|
+
// src/organize/cohesion/imports/extensions.ts
|
|
1701
|
+
function removeExtension(fileName) {
|
|
1702
|
+
const compoundExtensions = [".test.ts", ".test.tsx", ".spec.ts", ".spec.tsx"];
|
|
1703
|
+
for (const ext of compoundExtensions) {
|
|
1704
|
+
if (fileName.endsWith(ext)) {
|
|
1705
|
+
return fileName.slice(0, -ext.length);
|
|
1706
|
+
}
|
|
1707
|
+
}
|
|
1708
|
+
const singleExtensions = [".ts", ".tsx", ".js", ".jsx"];
|
|
1709
|
+
for (const ext of singleExtensions) {
|
|
1710
|
+
if (fileName.endsWith(ext)) {
|
|
1711
|
+
return fileName.slice(0, -ext.length);
|
|
1712
|
+
}
|
|
1713
|
+
}
|
|
1714
|
+
return fileName;
|
|
1715
|
+
}
|
|
1716
|
+
|
|
1717
|
+
// src/organize/cohesion/imports/resolve.ts
|
|
1718
|
+
function resolveImportToFile(importSpecifier, availableFiles) {
|
|
1719
|
+
if (!importSpecifier.startsWith("./")) {
|
|
1720
|
+
return void 0;
|
|
1721
|
+
}
|
|
1722
|
+
let relativePath = importSpecifier.slice(2);
|
|
1723
|
+
const compoundExtensions = [".test.ts", ".test.tsx", ".spec.ts", ".spec.tsx"];
|
|
1724
|
+
for (const ext of compoundExtensions) {
|
|
1725
|
+
if (relativePath.endsWith(ext)) {
|
|
1726
|
+
relativePath = relativePath.slice(0, -ext.length);
|
|
1727
|
+
break;
|
|
1728
|
+
}
|
|
1729
|
+
}
|
|
1730
|
+
const singleExtensions = [".ts", ".tsx", ".js", ".jsx"];
|
|
1731
|
+
for (const ext of singleExtensions) {
|
|
1732
|
+
if (relativePath.endsWith(ext)) {
|
|
1733
|
+
relativePath = relativePath.slice(0, -ext.length);
|
|
1734
|
+
break;
|
|
1735
|
+
}
|
|
1736
|
+
}
|
|
1737
|
+
return availableFiles.get(relativePath);
|
|
1738
|
+
}
|
|
1739
|
+
|
|
1740
|
+
// src/organize/cohesion/imports/graph.ts
|
|
1741
|
+
function addImportsToAdjacency(fileName, imports, adjacency, availableFiles) {
|
|
1742
|
+
for (const importSpecifier of imports) {
|
|
1743
|
+
const resolvedFileName = resolveImportToFile(importSpecifier, availableFiles);
|
|
1744
|
+
if (resolvedFileName) {
|
|
1745
|
+
adjacency.get(fileName).add(resolvedFileName);
|
|
1746
|
+
}
|
|
1747
|
+
}
|
|
1748
|
+
}
|
|
1749
|
+
function buildImportGraph(directoryPath, fileNames) {
|
|
1750
|
+
const adjacency = /* @__PURE__ */ new Map();
|
|
1751
|
+
for (const fileName of fileNames) {
|
|
1752
|
+
adjacency.set(fileName, /* @__PURE__ */ new Set());
|
|
1753
|
+
}
|
|
1754
|
+
const availableFiles = /* @__PURE__ */ new Map();
|
|
1755
|
+
for (const fileName of fileNames) {
|
|
1756
|
+
const baseName = removeExtension(fileName);
|
|
1757
|
+
availableFiles.set(baseName, fileName);
|
|
1758
|
+
}
|
|
1759
|
+
for (const fileName of fileNames) {
|
|
1760
|
+
const filePath = join15(directoryPath, fileName);
|
|
1761
|
+
const imports = parseFileImports(filePath, fileName);
|
|
1762
|
+
addImportsToAdjacency(fileName, imports, adjacency, availableFiles);
|
|
1763
|
+
}
|
|
1764
|
+
return adjacency;
|
|
1765
|
+
}
|
|
1766
|
+
|
|
1767
|
+
// src/organize/naming/stripExtension.ts
|
|
1768
|
+
function stripExtension(name) {
|
|
1769
|
+
const compoundExtensions = [".test.ts", ".test.tsx", ".spec.ts", ".spec.tsx"];
|
|
1770
|
+
for (const ext of compoundExtensions) {
|
|
1771
|
+
if (name.endsWith(ext)) {
|
|
1772
|
+
return name.slice(0, -ext.length);
|
|
1773
|
+
}
|
|
1774
|
+
}
|
|
1775
|
+
const singleExtensions = [".ts", ".tsx", ".js", ".jsx"];
|
|
1776
|
+
for (const ext of singleExtensions) {
|
|
1777
|
+
if (name.endsWith(ext)) {
|
|
1778
|
+
return name.slice(0, -ext.length);
|
|
1779
|
+
}
|
|
1780
|
+
}
|
|
1781
|
+
return name;
|
|
1782
|
+
}
|
|
1783
|
+
|
|
1784
|
+
// src/organize/naming/characters.ts
|
|
1785
|
+
var UPPERCASE_A = "A".charCodeAt(0);
|
|
1786
|
+
var UPPERCASE_Z = "Z".charCodeAt(0);
|
|
1787
|
+
var LOWERCASE_A = "a".charCodeAt(0);
|
|
1788
|
+
var LOWERCASE_Z = "z".charCodeAt(0);
|
|
1789
|
+
var DIGIT_ZERO = "0".charCodeAt(0);
|
|
1790
|
+
var DIGIT_NINE = "9".charCodeAt(0);
|
|
1791
|
+
function isUppercaseLetter(character) {
|
|
1792
|
+
const code = character.charCodeAt(0);
|
|
1793
|
+
return code >= UPPERCASE_A && code <= UPPERCASE_Z;
|
|
1794
|
+
}
|
|
1795
|
+
function isLowercaseLetter(character) {
|
|
1796
|
+
const code = character.charCodeAt(0);
|
|
1797
|
+
return code >= LOWERCASE_A && code <= LOWERCASE_Z;
|
|
1798
|
+
}
|
|
1799
|
+
function isLetter(character) {
|
|
1800
|
+
return isUppercaseLetter(character) || isLowercaseLetter(character);
|
|
1801
|
+
}
|
|
1802
|
+
function isDigit(character) {
|
|
1803
|
+
const code = character.charCodeAt(0);
|
|
1804
|
+
return code >= DIGIT_ZERO && code <= DIGIT_NINE;
|
|
1805
|
+
}
|
|
1806
|
+
function isTokenCharacter(character) {
|
|
1807
|
+
return isLetter(character) || isDigit(character);
|
|
1808
|
+
}
|
|
1809
|
+
|
|
1810
|
+
// src/organize/naming/boundaries.ts
|
|
1811
|
+
function isLowerToUpperBoundary(previous, current) {
|
|
1812
|
+
return isLowercaseLetter(previous) && isUppercaseLetter(current);
|
|
1813
|
+
}
|
|
1814
|
+
function isLetterToDigitBoundary(previous, current) {
|
|
1815
|
+
return isLetter(previous) && isDigit(current);
|
|
1816
|
+
}
|
|
1817
|
+
function isDigitToLetterBoundary(previous, current) {
|
|
1818
|
+
return isDigit(previous) && isLetter(current);
|
|
1819
|
+
}
|
|
1820
|
+
function isAcronymBoundary(previous, current, next) {
|
|
1821
|
+
if (next === void 0) {
|
|
1822
|
+
return false;
|
|
1823
|
+
}
|
|
1824
|
+
return isUppercaseLetter(previous) && isUppercaseLetter(current) && isLowercaseLetter(next);
|
|
1825
|
+
}
|
|
1826
|
+
function shouldStartNewToken(previous, current, next) {
|
|
1827
|
+
return isLowerToUpperBoundary(previous, current) || isLetterToDigitBoundary(previous, current) || isDigitToLetterBoundary(previous, current) || isAcronymBoundary(previous, current, next);
|
|
1828
|
+
}
|
|
1829
|
+
|
|
1830
|
+
// src/organize/naming/tokenize.ts
|
|
1831
|
+
function tokenize(name) {
|
|
1832
|
+
const withoutExtension = stripExtension(name);
|
|
1833
|
+
const characters = Array.from(withoutExtension);
|
|
1834
|
+
const tokens = [];
|
|
1835
|
+
let currentToken = "";
|
|
1836
|
+
characters.forEach((character, index) => {
|
|
1837
|
+
if (!isTokenCharacter(character)) {
|
|
1838
|
+
if (currentToken.length > 0) {
|
|
1839
|
+
tokens.push(currentToken.toLowerCase());
|
|
1840
|
+
currentToken = "";
|
|
1841
|
+
}
|
|
1842
|
+
return;
|
|
1843
|
+
}
|
|
1844
|
+
const previous = currentToken[currentToken.length - 1] ?? "";
|
|
1845
|
+
if (shouldStartNewToken(previous, character, characters[index + 1])) {
|
|
1846
|
+
tokens.push(currentToken.toLowerCase());
|
|
1847
|
+
currentToken = character;
|
|
1848
|
+
} else {
|
|
1849
|
+
currentToken += character;
|
|
1850
|
+
}
|
|
1851
|
+
});
|
|
1852
|
+
if (currentToken.length > 0) {
|
|
1853
|
+
tokens.push(currentToken.toLowerCase());
|
|
1854
|
+
}
|
|
1855
|
+
return tokens;
|
|
1856
|
+
}
|
|
1857
|
+
|
|
1858
|
+
// src/organize/cohesion/cluster/prefix.ts
|
|
1859
|
+
function buildPrefixGroups(fileNames) {
|
|
1860
|
+
const groups = /* @__PURE__ */ new Map();
|
|
1861
|
+
for (const fileName of fileNames) {
|
|
1862
|
+
const tokens = tokenize(fileName);
|
|
1863
|
+
if (tokens.length > 0) {
|
|
1864
|
+
const prefix = tokens[0];
|
|
1865
|
+
if (!groups.has(prefix)) {
|
|
1866
|
+
groups.set(prefix, /* @__PURE__ */ new Set());
|
|
1867
|
+
}
|
|
1868
|
+
groups.get(prefix).add(fileName);
|
|
1869
|
+
}
|
|
1870
|
+
}
|
|
1871
|
+
return groups;
|
|
1872
|
+
}
|
|
1873
|
+
function countFirstTokens(fileNames) {
|
|
1874
|
+
const tokenCounts = /* @__PURE__ */ new Map();
|
|
1875
|
+
for (const fileName of fileNames) {
|
|
1876
|
+
const tokens = tokenize(fileName);
|
|
1877
|
+
if (tokens.length > 0) {
|
|
1878
|
+
const token = tokens[0];
|
|
1879
|
+
tokenCounts.set(token, (tokenCounts.get(token) ?? 0) + 1);
|
|
1880
|
+
}
|
|
1881
|
+
}
|
|
1882
|
+
return tokenCounts;
|
|
1883
|
+
}
|
|
1884
|
+
function findMostCommonToken(tokenCounts) {
|
|
1885
|
+
let mostCommonToken = "";
|
|
1886
|
+
let maxCount = 0;
|
|
1887
|
+
for (const [token, count] of tokenCounts) {
|
|
1888
|
+
if (count > maxCount) {
|
|
1889
|
+
maxCount = count;
|
|
1890
|
+
mostCommonToken = token;
|
|
1891
|
+
}
|
|
1892
|
+
}
|
|
1893
|
+
return mostCommonToken;
|
|
1894
|
+
}
|
|
1895
|
+
function derivePrefix(fileNames) {
|
|
1896
|
+
if (fileNames.length === 0) {
|
|
1897
|
+
return "";
|
|
1898
|
+
}
|
|
1899
|
+
const tokenCounts = countFirstTokens(fileNames);
|
|
1900
|
+
const mostCommonToken = findMostCommonToken(tokenCounts);
|
|
1901
|
+
if (mostCommonToken.length > 0) {
|
|
1902
|
+
return mostCommonToken;
|
|
1903
|
+
}
|
|
1904
|
+
return fileNames[0];
|
|
1905
|
+
}
|
|
1906
|
+
|
|
1907
|
+
// src/organize/cohesion/cluster/components.ts
|
|
1908
|
+
function findImportComponents(fileNames, importGraph) {
|
|
1909
|
+
const visited = /* @__PURE__ */ new Set();
|
|
1910
|
+
const components = [];
|
|
1911
|
+
for (const fileName of fileNames) {
|
|
1912
|
+
if (!visited.has(fileName)) {
|
|
1913
|
+
const component = /* @__PURE__ */ new Set();
|
|
1914
|
+
bfsComponent(fileName, importGraph, visited, component);
|
|
1915
|
+
components.push(component);
|
|
1916
|
+
}
|
|
1917
|
+
}
|
|
1918
|
+
return components;
|
|
1919
|
+
}
|
|
1920
|
+
function bfsComponent(startFile, importGraph, visited, component) {
|
|
1921
|
+
const queue = [startFile];
|
|
1922
|
+
const queued = new Set(visited);
|
|
1923
|
+
queued.add(startFile);
|
|
1924
|
+
visited.add(startFile);
|
|
1925
|
+
component.add(startFile);
|
|
1926
|
+
while (queue.length > 0) {
|
|
1927
|
+
const current = queue.shift();
|
|
1928
|
+
const importedFiles = importGraph.get(current) ?? /* @__PURE__ */ new Set();
|
|
1929
|
+
for (const imported of importedFiles) {
|
|
1930
|
+
if (queued.has(imported)) {
|
|
1931
|
+
continue;
|
|
1932
|
+
}
|
|
1933
|
+
queued.add(imported);
|
|
1934
|
+
visited.add(imported);
|
|
1935
|
+
component.add(imported);
|
|
1936
|
+
queue.push(imported);
|
|
1937
|
+
}
|
|
1938
|
+
for (const [file, imports] of importGraph) {
|
|
1939
|
+
if (!imports.has(current) || queued.has(file)) {
|
|
1940
|
+
continue;
|
|
1941
|
+
}
|
|
1942
|
+
queued.add(file);
|
|
1943
|
+
visited.add(file);
|
|
1944
|
+
component.add(file);
|
|
1945
|
+
queue.push(file);
|
|
1946
|
+
}
|
|
1947
|
+
}
|
|
1948
|
+
}
|
|
1949
|
+
|
|
1950
|
+
// src/organize/cohesion/cluster/overlap.ts
|
|
1951
|
+
function isComponentCovered(component, assignedFiles) {
|
|
1952
|
+
for (const member of component) {
|
|
1953
|
+
if (assignedFiles.has(member)) {
|
|
1954
|
+
return true;
|
|
1955
|
+
}
|
|
1956
|
+
}
|
|
1957
|
+
return false;
|
|
1958
|
+
}
|
|
1959
|
+
function addComponentToAssigned(component, assignedFiles) {
|
|
1960
|
+
for (const member of component) {
|
|
1961
|
+
assignedFiles.add(member);
|
|
1962
|
+
}
|
|
1963
|
+
}
|
|
1964
|
+
function findOverlappingComponent(members, components) {
|
|
1965
|
+
for (const component of components) {
|
|
1966
|
+
for (const member of members) {
|
|
1967
|
+
if (component.has(member)) {
|
|
1968
|
+
return component;
|
|
1969
|
+
}
|
|
1970
|
+
}
|
|
1971
|
+
}
|
|
1972
|
+
return void 0;
|
|
1973
|
+
}
|
|
1974
|
+
function hasSignificantOverlap(set1, set2) {
|
|
1975
|
+
const smallerSize = Math.min(set1.size, set2.size);
|
|
1976
|
+
const threshold = Math.ceil(smallerSize * 50 / 100);
|
|
1977
|
+
let overlapCount = 0;
|
|
1978
|
+
for (const item of set1) {
|
|
1979
|
+
if (set1.has(item) && set2.has(item)) {
|
|
1980
|
+
overlapCount++;
|
|
1981
|
+
}
|
|
1982
|
+
}
|
|
1983
|
+
return overlapCount >= threshold;
|
|
1984
|
+
}
|
|
1985
|
+
|
|
1986
|
+
// src/organize/cohesion/cluster/find.ts
|
|
1987
|
+
function createPrefixCluster(prefix, members, validImportComponents) {
|
|
1988
|
+
const memberArray = Array.from(members).sort();
|
|
1989
|
+
const overlapComponent = findOverlappingComponent(members, validImportComponents);
|
|
1990
|
+
const confidence = overlapComponent && hasSignificantOverlap(members, overlapComponent) ? "prefix+imports" : "prefix-only";
|
|
1991
|
+
return {
|
|
1992
|
+
prefix,
|
|
1993
|
+
members: memberArray,
|
|
1994
|
+
memberCount: memberArray.length,
|
|
1995
|
+
suggestedFolder: prefix.toLowerCase(),
|
|
1996
|
+
confidence
|
|
1997
|
+
};
|
|
1998
|
+
}
|
|
1999
|
+
function createImportCluster(component) {
|
|
2000
|
+
const memberArray = Array.from(component).sort();
|
|
2001
|
+
const prefix = derivePrefix(memberArray);
|
|
2002
|
+
return {
|
|
2003
|
+
prefix,
|
|
2004
|
+
members: memberArray,
|
|
2005
|
+
memberCount: memberArray.length,
|
|
2006
|
+
suggestedFolder: prefix.toLowerCase(),
|
|
2007
|
+
confidence: "imports-only"
|
|
2008
|
+
};
|
|
2009
|
+
}
|
|
2010
|
+
function findCohesionClusters(fileNames, importGraph, minClusterSize) {
|
|
2011
|
+
const clusters = [];
|
|
2012
|
+
const prefixGroups = buildPrefixGroups(fileNames);
|
|
2013
|
+
const validPrefixGroups = /* @__PURE__ */ new Map();
|
|
2014
|
+
for (const [prefix, members] of prefixGroups) {
|
|
2015
|
+
if (members.size >= minClusterSize) {
|
|
2016
|
+
validPrefixGroups.set(prefix, members);
|
|
2017
|
+
}
|
|
2018
|
+
}
|
|
2019
|
+
const importComponents = findImportComponents(fileNames, importGraph);
|
|
2020
|
+
const validImportComponents = importComponents.filter((component) => component.size >= minClusterSize);
|
|
2021
|
+
const assignedFiles = /* @__PURE__ */ new Set();
|
|
2022
|
+
for (const [prefix, members] of validPrefixGroups) {
|
|
2023
|
+
const cluster = createPrefixCluster(prefix, members, validImportComponents);
|
|
2024
|
+
clusters.push(cluster);
|
|
2025
|
+
addComponentToAssigned(members, assignedFiles);
|
|
2026
|
+
}
|
|
2027
|
+
for (const component of validImportComponents) {
|
|
2028
|
+
if (!isComponentCovered(component, assignedFiles)) {
|
|
2029
|
+
const cluster = createImportCluster(component);
|
|
2030
|
+
clusters.push(cluster);
|
|
2031
|
+
addComponentToAssigned(component, assignedFiles);
|
|
2032
|
+
}
|
|
2033
|
+
}
|
|
2034
|
+
clusters.sort((clusterA, clusterB) => {
|
|
2035
|
+
if (clusterA.memberCount !== clusterB.memberCount) {
|
|
2036
|
+
return clusterB.memberCount - clusterA.memberCount;
|
|
2037
|
+
}
|
|
2038
|
+
return clusterA.prefix.localeCompare(clusterB.prefix);
|
|
2039
|
+
});
|
|
2040
|
+
return clusters;
|
|
2041
|
+
}
|
|
2042
|
+
|
|
2043
|
+
// src/organize/metric/naming/redundancy.ts
|
|
2044
|
+
import { basename as basename5 } from "path";
|
|
2045
|
+
|
|
2046
|
+
// src/organize/metric/naming/conventional.ts
|
|
2047
|
+
import { basename as basename4 } from "path";
|
|
2048
|
+
|
|
2049
|
+
// src/organize/metric/naming/nameStrip.ts
|
|
2050
|
+
function stripExtension2(fileName) {
|
|
2051
|
+
let baseName = fileName;
|
|
2052
|
+
baseName = baseName.replace(/\.(test|spec)\.(ts|tsx|js|jsx)$/, "");
|
|
2053
|
+
if (baseName === fileName) {
|
|
2054
|
+
const lastDot = baseName.lastIndexOf(".");
|
|
2055
|
+
if (lastDot > 0) {
|
|
2056
|
+
baseName = baseName.slice(0, lastDot);
|
|
2057
|
+
}
|
|
2058
|
+
}
|
|
2059
|
+
return baseName;
|
|
2060
|
+
}
|
|
2061
|
+
|
|
2062
|
+
// src/organize/metric/naming/conventional.ts
|
|
2063
|
+
function isConventionalEntryFile(filePath, ancestorFolders) {
|
|
2064
|
+
const fileStem = stripExtension2(basename4(filePath));
|
|
2065
|
+
const lowerStem = fileStem.toLowerCase();
|
|
2066
|
+
const lowerAncestors = ancestorFolders.map((folder) => folder.toLowerCase());
|
|
2067
|
+
if (lowerStem === "index") {
|
|
2068
|
+
return true;
|
|
2069
|
+
}
|
|
2070
|
+
if (lowerStem === "app") {
|
|
2071
|
+
return lowerAncestors.includes("app");
|
|
2072
|
+
}
|
|
2073
|
+
if (lowerStem === "export") {
|
|
2074
|
+
return lowerAncestors.includes("export");
|
|
2075
|
+
}
|
|
2076
|
+
if (!lowerStem.startsWith("use")) {
|
|
2077
|
+
return false;
|
|
2078
|
+
}
|
|
2079
|
+
const hookName = fileStem.slice(3);
|
|
2080
|
+
const hookTokens = tokenize(hookName);
|
|
2081
|
+
return hookTokens.some((hookToken) => ancestorFolders.some((folder) => {
|
|
2082
|
+
const folderTokens = tokenize(folder);
|
|
2083
|
+
return folderTokens.includes(hookToken);
|
|
2084
|
+
}));
|
|
2085
|
+
}
|
|
2086
|
+
|
|
2087
|
+
// src/organize/metric/naming/redundancy.ts
|
|
2088
|
+
function pathRedundancy(filePath, ancestorFolders) {
|
|
2089
|
+
if (isConventionalEntryFile(filePath, ancestorFolders)) {
|
|
2090
|
+
return 0;
|
|
2091
|
+
}
|
|
2092
|
+
const fileName = basename5(filePath);
|
|
2093
|
+
const fileTokens = tokenize(fileName);
|
|
2094
|
+
if (fileTokens.length === 0) {
|
|
2095
|
+
return 0;
|
|
2096
|
+
}
|
|
2097
|
+
const ancestorTokens = /* @__PURE__ */ new Set();
|
|
2098
|
+
for (const folder of ancestorFolders) {
|
|
2099
|
+
const folderTokens = tokenize(folder);
|
|
2100
|
+
for (const token of folderTokens) {
|
|
2101
|
+
ancestorTokens.add(token);
|
|
2102
|
+
}
|
|
2103
|
+
}
|
|
2104
|
+
const sharedCount = fileTokens.filter((token) => ancestorTokens.has(token)).length;
|
|
2105
|
+
return sharedCount / fileTokens.length;
|
|
2106
|
+
}
|
|
2107
|
+
|
|
2108
|
+
// src/organize/analyze/ancestors.ts
|
|
2109
|
+
function extractAncestorFolders(directoryPath) {
|
|
2110
|
+
if (directoryPath === ".") {
|
|
2111
|
+
return [];
|
|
2112
|
+
}
|
|
2113
|
+
return directoryPath.split(/[/\\]/).filter((seg) => seg.length > 0);
|
|
2114
|
+
}
|
|
2115
|
+
function computeAverageRedundancy(fileNames, ancestorFolders) {
|
|
2116
|
+
const redundancyScores = fileNames.map((fileName) => pathRedundancy(fileName, ancestorFolders));
|
|
2117
|
+
if (redundancyScores.length === 0) {
|
|
2118
|
+
return 0;
|
|
2119
|
+
}
|
|
2120
|
+
const sum = redundancyScores.reduce((total, score) => total + score, 0);
|
|
2121
|
+
return sum / redundancyScores.length;
|
|
2122
|
+
}
|
|
2123
|
+
|
|
2124
|
+
// src/organize/analyze/issues.ts
|
|
2125
|
+
import { readFileSync as readFileSync8 } from "fs";
|
|
2126
|
+
|
|
2127
|
+
// src/organize/metric/naming/details.ts
|
|
2128
|
+
var LOW_INFO_NAME_DETAILS = {
|
|
2129
|
+
utils: "Catch-all dumping ground; violates single responsibility",
|
|
2130
|
+
helpers: "Vague semantics; becomes unmaintainable",
|
|
2131
|
+
misc: "Literally means 'uncategorized'",
|
|
2132
|
+
common: "Attracts unrelated shared code",
|
|
2133
|
+
shared: "Breaks architectural layers; grows uncontrollably",
|
|
2134
|
+
_shared: "Variant of shared with same problems",
|
|
2135
|
+
lib: "Too generic; doesn't describe contents",
|
|
2136
|
+
index: "Indistinguishable in IDE tabs; breaks Go to Definition",
|
|
2137
|
+
types: "Can become a dump for unrelated type definitions",
|
|
2138
|
+
constants: "Can become a dump for unrelated values",
|
|
2139
|
+
config: "Vague without domain context",
|
|
2140
|
+
base: "Abstract without inheritance context",
|
|
2141
|
+
core: "Too broad; doesn't narrow scope"
|
|
2142
|
+
};
|
|
2143
|
+
|
|
2144
|
+
// src/organize/metric/naming/lowInfo.ts
|
|
2145
|
+
function checkLowInfoName(fileName, config, isPackageEntryPoint) {
|
|
2146
|
+
const baseName = stripExtension2(fileName);
|
|
2147
|
+
const lowerBaseName = baseName.toLowerCase();
|
|
2148
|
+
if (lowerBaseName === "index" && isPackageEntryPoint) {
|
|
2149
|
+
return void 0;
|
|
2150
|
+
}
|
|
2151
|
+
const bannedIndex = config.banned.findIndex((name) => name.toLowerCase() === lowerBaseName);
|
|
2152
|
+
if (bannedIndex >= 0) {
|
|
2153
|
+
const bannedName = config.banned[bannedIndex];
|
|
2154
|
+
const detail = LOW_INFO_NAME_DETAILS[bannedName] ?? "Low-information filename";
|
|
2155
|
+
return {
|
|
2156
|
+
detail,
|
|
2157
|
+
fileName,
|
|
2158
|
+
kind: "low-info-banned"
|
|
2159
|
+
};
|
|
2160
|
+
}
|
|
2161
|
+
const discouragedIndex = config.discouraged.findIndex((name) => name.toLowerCase() === lowerBaseName);
|
|
2162
|
+
if (discouragedIndex >= 0) {
|
|
2163
|
+
const discouragedName = config.discouraged[discouragedIndex];
|
|
2164
|
+
const detail = LOW_INFO_NAME_DETAILS[discouragedName] ?? "Low-information filename";
|
|
2165
|
+
return {
|
|
2166
|
+
detail,
|
|
2167
|
+
fileName,
|
|
2168
|
+
kind: "low-info-discouraged"
|
|
2169
|
+
};
|
|
2170
|
+
}
|
|
2171
|
+
return void 0;
|
|
2172
|
+
}
|
|
2173
|
+
|
|
2174
|
+
// src/organize/metric/barrel/detection.ts
|
|
2175
|
+
import * as ts9 from "typescript";
|
|
2176
|
+
|
|
2177
|
+
// src/organize/metric/barrel/reExport.ts
|
|
2178
|
+
import * as ts8 from "typescript";
|
|
2179
|
+
var SUPPORTED_EXTENSIONS = /* @__PURE__ */ new Set([".ts", ".tsx", ".js", ".jsx"]);
|
|
2180
|
+
function getFileExtension(fileName) {
|
|
2181
|
+
const lastDot = fileName.lastIndexOf(".");
|
|
2182
|
+
return lastDot > 0 ? fileName.slice(lastDot) : "";
|
|
2183
|
+
}
|
|
2184
|
+
function isReExportStatement(statement) {
|
|
2185
|
+
if (!ts8.isExportDeclaration(statement)) {
|
|
2186
|
+
return false;
|
|
2187
|
+
}
|
|
2188
|
+
if (statement.moduleSpecifier) {
|
|
2189
|
+
return true;
|
|
2190
|
+
}
|
|
2191
|
+
const exportClause = statement.exportClause;
|
|
2192
|
+
if (exportClause === void 0) {
|
|
2193
|
+
return false;
|
|
2194
|
+
}
|
|
2195
|
+
if (!ts8.isNamedExports(exportClause)) {
|
|
2196
|
+
return false;
|
|
2197
|
+
}
|
|
2198
|
+
return exportClause.elements.length > 0;
|
|
2199
|
+
}
|
|
2200
|
+
|
|
2201
|
+
// src/organize/metric/barrel/detection.ts
|
|
2202
|
+
function scriptKindForExtension(ext) {
|
|
2203
|
+
if (ext === ".tsx") {
|
|
2204
|
+
return ts9.ScriptKind.TSX;
|
|
2205
|
+
}
|
|
2206
|
+
if (ext === ".jsx") {
|
|
2207
|
+
return ts9.ScriptKind.JSX;
|
|
2208
|
+
}
|
|
2209
|
+
if (ext === ".js") {
|
|
2210
|
+
return ts9.ScriptKind.JS;
|
|
2211
|
+
}
|
|
2212
|
+
return ts9.ScriptKind.TS;
|
|
2213
|
+
}
|
|
2214
|
+
function checkBarrelFile(fileName, fileContent) {
|
|
2215
|
+
const ext = getFileExtension(fileName);
|
|
2216
|
+
if (!SUPPORTED_EXTENSIONS.has(ext)) {
|
|
2217
|
+
return void 0;
|
|
2218
|
+
}
|
|
2219
|
+
const sourceFile = ts9.createSourceFile(
|
|
2220
|
+
fileName,
|
|
2221
|
+
fileContent,
|
|
2222
|
+
ts9.ScriptTarget.Latest,
|
|
2223
|
+
void 0,
|
|
2224
|
+
scriptKindForExtension(ext)
|
|
2225
|
+
);
|
|
2226
|
+
let totalStatements = 0;
|
|
2227
|
+
let reExportCount = 0;
|
|
2228
|
+
for (const statement of sourceFile.statements) {
|
|
2229
|
+
if (!ts9.isModuleDeclaration(statement) && !ts9.isNamespaceExport(statement)) {
|
|
2230
|
+
totalStatements++;
|
|
2231
|
+
}
|
|
2232
|
+
if (isReExportStatement(statement)) {
|
|
2233
|
+
reExportCount++;
|
|
2234
|
+
}
|
|
2235
|
+
}
|
|
2236
|
+
const reExportRatio = reExportCount / totalStatements;
|
|
2237
|
+
if (reExportRatio >= 0.8) {
|
|
2238
|
+
const detail = `80% of statements are re-exports (${reExportCount} of ${totalStatements})`;
|
|
2239
|
+
return {
|
|
2240
|
+
detail,
|
|
2241
|
+
fileName,
|
|
2242
|
+
kind: "barrel"
|
|
2243
|
+
};
|
|
2244
|
+
}
|
|
2245
|
+
return void 0;
|
|
2246
|
+
}
|
|
2247
|
+
|
|
2248
|
+
// src/organize/analyze/issues.ts
|
|
2249
|
+
function collectFileIssues(fileNames, directoryPath, ancestorFolders, lowInfoNames, redundancyThreshold, isPackageEntryDirectory = true) {
|
|
2250
|
+
const issues = [];
|
|
2251
|
+
for (const fileName of fileNames) {
|
|
2252
|
+
const score = pathRedundancy(fileName, ancestorFolders);
|
|
2253
|
+
if (score >= redundancyThreshold) {
|
|
2254
|
+
issues.push({
|
|
2255
|
+
fileName,
|
|
2256
|
+
kind: "redundancy",
|
|
2257
|
+
detail: `filename repeats path context (${(score * 100).toFixed(0)}% token overlap)`,
|
|
2258
|
+
redundancyScore: score
|
|
2259
|
+
});
|
|
2260
|
+
}
|
|
2261
|
+
const lowInfoIssue = checkLowInfoName(fileName, lowInfoNames, isPackageEntryDirectory);
|
|
2262
|
+
if (lowInfoIssue) {
|
|
2263
|
+
issues.push(lowInfoIssue);
|
|
2264
|
+
}
|
|
2265
|
+
try {
|
|
2266
|
+
const filePath = `${directoryPath}/${fileName}`;
|
|
2267
|
+
const fileContent = readFileSync8(filePath, "utf-8");
|
|
2268
|
+
const barrelIssue = checkBarrelFile(fileName, fileContent);
|
|
2269
|
+
if (barrelIssue) {
|
|
2270
|
+
issues.push(barrelIssue);
|
|
2271
|
+
}
|
|
2272
|
+
} catch {
|
|
2273
|
+
}
|
|
2274
|
+
}
|
|
2275
|
+
return issues;
|
|
2276
|
+
}
|
|
2277
|
+
|
|
2278
|
+
// src/organize/analyze/run.ts
|
|
2279
|
+
function analyze(target) {
|
|
2280
|
+
const config = loadOrganizeConfig(REPO_ROOT, target.packageName);
|
|
2281
|
+
const entries = walkDirectories(target.absolutePath);
|
|
2282
|
+
const metrics = [];
|
|
2283
|
+
for (const entry of entries) {
|
|
2284
|
+
const directoryPath = entry.directoryPath === target.absolutePath ? "." : relative7(target.absolutePath, entry.directoryPath);
|
|
2285
|
+
const fileFanOut = entry.files.length;
|
|
2286
|
+
const fileFanOutVerd = fileFanOutVerdict(fileFanOut, config.fileFanOut.warning, config.fileFanOut.split);
|
|
2287
|
+
const folderFanOut = entry.subdirectories.length;
|
|
2288
|
+
const folderFanOutVerd = folderFanOutVerdict(folderFanOut, config.folderFanOut.warning, config.folderFanOut.split);
|
|
2289
|
+
const depth = directoryDepth(entry.directoryPath, target.absolutePath);
|
|
2290
|
+
const depthVerd = depthVerdict(depth, config.depth.warning, config.depth.deep);
|
|
2291
|
+
const ancestorFolders = extractAncestorFolders(directoryPath);
|
|
2292
|
+
const averageRedundancy = computeAverageRedundancy(entry.files, ancestorFolders);
|
|
2293
|
+
const fileIssues = collectFileIssues(
|
|
2294
|
+
entry.files,
|
|
2295
|
+
entry.directoryPath,
|
|
2296
|
+
ancestorFolders,
|
|
2297
|
+
config.lowInfoNames,
|
|
2298
|
+
config.redundancyThreshold,
|
|
2299
|
+
entry.directoryPath === target.absolutePath
|
|
2300
|
+
);
|
|
2301
|
+
const importGraph = buildImportGraph(entry.directoryPath, entry.files);
|
|
2302
|
+
const clusters = findCohesionClusters(entry.files, importGraph, config.cohesionClusterMinSize);
|
|
2303
|
+
metrics.push({
|
|
2304
|
+
averageRedundancy: Math.round(averageRedundancy * 100) / 100,
|
|
2305
|
+
clusters,
|
|
2306
|
+
depth,
|
|
2307
|
+
depthVerdict: depthVerd,
|
|
2308
|
+
directoryPath,
|
|
2309
|
+
fileIssues,
|
|
2310
|
+
fileFanOut,
|
|
2311
|
+
fileFanOutVerdict: fileFanOutVerd,
|
|
2312
|
+
folderFanOut,
|
|
2313
|
+
folderFanOutVerdict: folderFanOutVerd
|
|
2314
|
+
});
|
|
2315
|
+
}
|
|
2316
|
+
return metrics;
|
|
2317
|
+
}
|
|
2318
|
+
|
|
2319
|
+
// src/organize/compare/baseline.ts
|
|
2320
|
+
import { readFileSync as readFileSync9 } from "fs";
|
|
2321
|
+
|
|
2322
|
+
// src/organize/compare/verdict.ts
|
|
2323
|
+
function verdictFromDeltas(fileFanOutDelta, folderFanOutDelta, clusterCountDelta, issueCountDelta, redundancyDelta) {
|
|
2324
|
+
const deltas = [fileFanOutDelta, folderFanOutDelta, clusterCountDelta, issueCountDelta, redundancyDelta];
|
|
2325
|
+
let direction = 0;
|
|
2326
|
+
for (const delta of deltas) {
|
|
2327
|
+
const nextDirection = Math.sign(delta);
|
|
2328
|
+
if (nextDirection === 0) {
|
|
2329
|
+
continue;
|
|
2330
|
+
}
|
|
2331
|
+
if (direction === 0) {
|
|
2332
|
+
direction = nextDirection;
|
|
2333
|
+
continue;
|
|
2334
|
+
}
|
|
2335
|
+
if (direction !== nextDirection) {
|
|
2336
|
+
return "mixed";
|
|
2337
|
+
}
|
|
2338
|
+
}
|
|
2339
|
+
if (direction < 0) {
|
|
2340
|
+
return "improved";
|
|
2341
|
+
}
|
|
2342
|
+
if (direction > 0) {
|
|
2343
|
+
return "worse";
|
|
2344
|
+
}
|
|
2345
|
+
return "unchanged";
|
|
2346
|
+
}
|
|
2347
|
+
|
|
2348
|
+
// src/organize/compare/baseline.ts
|
|
2349
|
+
function roundedDelta(current, previous) {
|
|
2350
|
+
return Math.round((current - previous) * 100) / 100;
|
|
2351
|
+
}
|
|
2352
|
+
function baselineMetricsByPath(baseline2) {
|
|
2353
|
+
return new Map(baseline2.map((metric) => [metric.directoryPath, metric]));
|
|
2354
|
+
}
|
|
2355
|
+
function compareBaseline(current, baselinePath) {
|
|
2356
|
+
const baselineData = JSON.parse(readFileSync9(baselinePath, "utf-8"));
|
|
2357
|
+
const previousByPath = baselineMetricsByPath(baselineData);
|
|
2358
|
+
const comparisons = /* @__PURE__ */ new Map();
|
|
2359
|
+
for (const metric of current) {
|
|
2360
|
+
const previous = previousByPath.get(metric.directoryPath);
|
|
2361
|
+
if (!previous) {
|
|
2362
|
+
continue;
|
|
2363
|
+
}
|
|
2364
|
+
const fileFanOutDelta = metric.fileFanOut - previous.fileFanOut;
|
|
2365
|
+
const folderFanOutDelta = metric.folderFanOut - previous.folderFanOut;
|
|
2366
|
+
const clusterCountDelta = metric.clusters.length - previous.clusters.length;
|
|
2367
|
+
const issueCountDelta = metric.fileIssues.length - previous.fileIssues.length;
|
|
2368
|
+
const redundancyDelta = roundedDelta(metric.averageRedundancy, previous.averageRedundancy);
|
|
2369
|
+
const comparison = {
|
|
2370
|
+
fileFanOutDelta,
|
|
2371
|
+
folderFanOutDelta,
|
|
2372
|
+
clusterCountDelta,
|
|
2373
|
+
issueCountDelta,
|
|
2374
|
+
redundancyDelta,
|
|
2375
|
+
verdict: verdictFromDeltas(fileFanOutDelta, folderFanOutDelta, clusterCountDelta, issueCountDelta, redundancyDelta)
|
|
2376
|
+
};
|
|
2377
|
+
comparisons.set(metric.directoryPath, comparison);
|
|
2378
|
+
}
|
|
2379
|
+
return comparisons;
|
|
2380
|
+
}
|
|
2381
|
+
|
|
2382
|
+
// src/organize/report/clusters.ts
|
|
2383
|
+
function clusterLines(clusters, directoryPath) {
|
|
2384
|
+
const lines = [];
|
|
2385
|
+
const indent = " Clusters: ";
|
|
2386
|
+
const indentAlignment = " ".repeat(indent.length);
|
|
2387
|
+
for (let i = 0; i < clusters.length; i++) {
|
|
2388
|
+
const cluster = clusters[i];
|
|
2389
|
+
const confidence = cluster.confidence;
|
|
2390
|
+
const memberCount = cluster.memberCount;
|
|
2391
|
+
const base = directoryPath.endsWith("/") ? directoryPath : `${directoryPath}/`;
|
|
2392
|
+
const suggestedPath = `${base}${cluster.prefix}/`;
|
|
2393
|
+
const clusterLine = `${cluster.prefix} (${memberCount} files, ${confidence}) \u2192 suggest ${suggestedPath}`;
|
|
2394
|
+
if (i === 0) {
|
|
2395
|
+
lines.push(`${indent}${clusterLine}`);
|
|
2396
|
+
} else {
|
|
2397
|
+
lines.push(`${indentAlignment}${clusterLine}`);
|
|
2398
|
+
}
|
|
2399
|
+
}
|
|
2400
|
+
return lines;
|
|
2401
|
+
}
|
|
2402
|
+
|
|
2403
|
+
// src/organize/report/issueFormatters.ts
|
|
2404
|
+
function formatRedundancyIssues(issues) {
|
|
2405
|
+
if (issues.length === 0) {
|
|
2406
|
+
return void 0;
|
|
2407
|
+
}
|
|
2408
|
+
const itemsList = issues.map((issue2) => `${issue2.fileName} (${issue2.redundancyScore?.toFixed(2)})`).join(", ");
|
|
2409
|
+
return ` Redundant: ${itemsList}`;
|
|
2410
|
+
}
|
|
2411
|
+
function formatLowInfoIssues(issues) {
|
|
2412
|
+
if (issues.length === 0) {
|
|
2413
|
+
return void 0;
|
|
2414
|
+
}
|
|
2415
|
+
const itemsList = issues.map((issue2) => {
|
|
2416
|
+
const prefix = issue2.kind === "low-info-banned" ? "banned" : "discouraged";
|
|
2417
|
+
return `${issue2.fileName} (${prefix}: ${issue2.detail})`;
|
|
2418
|
+
}).join(", ");
|
|
2419
|
+
return ` Low-info: ${itemsList}`;
|
|
2420
|
+
}
|
|
2421
|
+
function formatBarrelIssues(issues) {
|
|
2422
|
+
if (issues.length === 0) {
|
|
2423
|
+
return void 0;
|
|
2424
|
+
}
|
|
2425
|
+
const itemsList = issues.map((issue2) => `${issue2.fileName} (${issue2.detail})`).join(", ");
|
|
2426
|
+
return ` Barrels: ${itemsList}`;
|
|
2427
|
+
}
|
|
2428
|
+
|
|
2429
|
+
// src/organize/report/fileIssues.ts
|
|
2430
|
+
function fileIssueLines(issues) {
|
|
2431
|
+
const redundancyIssues = issues.filter((i) => i.kind === "redundancy");
|
|
2432
|
+
const lowInfoIssues = issues.filter((i) => i.kind === "low-info-banned" || i.kind === "low-info-discouraged");
|
|
2433
|
+
const barrelIssues = issues.filter((i) => i.kind === "barrel");
|
|
2434
|
+
const lines = [];
|
|
2435
|
+
const redundancyLine = formatRedundancyIssues(redundancyIssues);
|
|
2436
|
+
if (redundancyLine) {
|
|
2437
|
+
lines.push(redundancyLine);
|
|
2438
|
+
}
|
|
2439
|
+
const lowInfoLine = formatLowInfoIssues(lowInfoIssues);
|
|
2440
|
+
if (lowInfoLine) {
|
|
2441
|
+
lines.push(lowInfoLine);
|
|
2442
|
+
}
|
|
2443
|
+
const barrelLine = formatBarrelIssues(barrelIssues);
|
|
2444
|
+
if (barrelLine) {
|
|
2445
|
+
lines.push(barrelLine);
|
|
2446
|
+
}
|
|
2447
|
+
return lines;
|
|
2448
|
+
}
|
|
2449
|
+
|
|
2450
|
+
// src/organize/report/summary.ts
|
|
2451
|
+
function worstVerdict(metric) {
|
|
2452
|
+
const depthVerdict2 = metric.depthVerdict === "DEEP" ? "SPLIT" : metric.depthVerdict;
|
|
2453
|
+
const verdicts = [depthVerdict2, metric.fileFanOutVerdict, metric.folderFanOutVerdict];
|
|
2454
|
+
if (verdicts.includes("SPLIT")) {
|
|
2455
|
+
return "SPLIT";
|
|
2456
|
+
}
|
|
2457
|
+
if (verdicts.includes("WARNING")) {
|
|
2458
|
+
return "WARNING";
|
|
2459
|
+
}
|
|
2460
|
+
return "STABLE";
|
|
2461
|
+
}
|
|
2462
|
+
function countIssuesByKind(metric, kindPrefix) {
|
|
2463
|
+
return metric.fileIssues.filter((issue2) => issue2.kind.startsWith(kindPrefix)).length;
|
|
2464
|
+
}
|
|
2465
|
+
function summaryLines2(metric) {
|
|
2466
|
+
const verdict = worstVerdict(metric);
|
|
2467
|
+
const redundantCount = countIssuesByKind(metric, "redundancy");
|
|
2468
|
+
const lowInfoCount = countIssuesByKind(metric, "low-info");
|
|
2469
|
+
const barrelCount = countIssuesByKind(metric, "barrel");
|
|
2470
|
+
const redundancy = metric.averageRedundancy.toFixed(2);
|
|
2471
|
+
const line = `${metric.directoryPath} [${verdict}] files: ${metric.fileFanOut} folders: ${metric.folderFanOut} depth: ${metric.depth} redundancy: ${redundancy} clusters: ${metric.clusters.length} redundant: ${redundantCount} low-info: ${lowInfoCount} barrels: ${barrelCount}`;
|
|
2472
|
+
return [line];
|
|
2473
|
+
}
|
|
2474
|
+
|
|
2475
|
+
// src/organize/report/format.ts
|
|
2476
|
+
function logLines2(lines) {
|
|
2477
|
+
for (const line of lines) {
|
|
2478
|
+
console.log(line);
|
|
2479
|
+
}
|
|
2480
|
+
}
|
|
2481
|
+
function shouldShowDirectory(metric, verbose) {
|
|
2482
|
+
if (verbose) {
|
|
2483
|
+
return true;
|
|
2484
|
+
}
|
|
2485
|
+
if (metric.fileIssues.length > 0) {
|
|
2486
|
+
return true;
|
|
2487
|
+
}
|
|
2488
|
+
const allVerdictStable = metric.fileFanOutVerdict === "STABLE" && metric.folderFanOutVerdict === "STABLE" && metric.depthVerdict === "STABLE";
|
|
2489
|
+
return !allVerdictStable;
|
|
2490
|
+
}
|
|
2491
|
+
function reportOrganize(metrics, options = {}) {
|
|
2492
|
+
const metricsToShow = metrics.filter((metric) => shouldShowDirectory(metric, options.verbose ?? false));
|
|
2493
|
+
if (metricsToShow.length === 0) {
|
|
2494
|
+
console.log("No directories found for organize analysis.");
|
|
2495
|
+
return;
|
|
2496
|
+
}
|
|
2497
|
+
for (const metric of metricsToShow) {
|
|
2498
|
+
logLines2(summaryLines2(metric));
|
|
2499
|
+
logLines2(clusterLines(metric.clusters, metric.directoryPath));
|
|
2500
|
+
logLines2(fileIssueLines(metric.fileIssues));
|
|
2501
|
+
console.log("");
|
|
2502
|
+
}
|
|
2503
|
+
}
|
|
2504
|
+
|
|
2505
|
+
// src/organize/command.ts
|
|
2506
|
+
var DEFAULT_DEPENDENCIES3 = {
|
|
2507
|
+
analyze,
|
|
2508
|
+
compareBaseline,
|
|
2509
|
+
mkdirSync: mkdirSync2,
|
|
2510
|
+
reportOrganize,
|
|
2511
|
+
resolveQualityTarget,
|
|
2512
|
+
writeFileSync: writeFileSync2
|
|
2513
|
+
};
|
|
2514
|
+
function baselineReportTarget(targetRelativePath) {
|
|
2515
|
+
if (targetRelativePath === ".") {
|
|
2516
|
+
return "repo";
|
|
2517
|
+
}
|
|
2518
|
+
return targetRelativePath;
|
|
2519
|
+
}
|
|
2520
|
+
function baselinePathFor(targetRelativePath) {
|
|
2521
|
+
const reportKey = sanitizeReportKey(baselineReportTarget(targetRelativePath));
|
|
2522
|
+
return resolveReportPath(REPO_ROOT, "organize", `${reportKey}.json`);
|
|
2523
|
+
}
|
|
2524
|
+
function stripComparisonsForBaseline(metrics) {
|
|
2525
|
+
return metrics.map(({ comparison: _comparison, ...rest }) => rest);
|
|
2526
|
+
}
|
|
2527
|
+
function runOrganizeCli(rawArgs, dependencies = DEFAULT_DEPENDENCIES3) {
|
|
2528
|
+
const args2 = cleanCliArgs(rawArgs);
|
|
2529
|
+
const target = dependencies.resolveQualityTarget(REPO_ROOT, parseTargetArg(args2, ["--compare"]));
|
|
2530
|
+
const verbose = args2.includes("--verbose");
|
|
2531
|
+
const writeBaseline = args2.includes("--write-baseline");
|
|
2532
|
+
const comparePath = flagValue(args2, "--compare");
|
|
2533
|
+
let metrics = dependencies.analyze(target);
|
|
2534
|
+
if (comparePath) {
|
|
2535
|
+
const comparisons = dependencies.compareBaseline(metrics, comparePath);
|
|
2536
|
+
const metricsWithComparisons = metrics.map((metric) => ({
|
|
2537
|
+
...metric,
|
|
2538
|
+
comparison: comparisons.get(metric.directoryPath)
|
|
2539
|
+
}));
|
|
2540
|
+
metrics = metricsWithComparisons;
|
|
2541
|
+
}
|
|
2542
|
+
if (writeBaseline) {
|
|
2543
|
+
const baselinePath = baselinePathFor(target.relativePath);
|
|
2544
|
+
dependencies.mkdirSync(join16(baselinePath, ".."), { recursive: true });
|
|
2545
|
+
const baseMetrics = stripComparisonsForBaseline(metrics);
|
|
2546
|
+
dependencies.writeFileSync(baselinePath, JSON.stringify(baseMetrics, null, 2));
|
|
2547
|
+
}
|
|
2548
|
+
if (args2.includes("--json")) {
|
|
2549
|
+
console.log(JSON.stringify(metrics, null, 2));
|
|
2550
|
+
return;
|
|
2551
|
+
}
|
|
2552
|
+
dependencies.reportOrganize(metrics, { verbose });
|
|
2553
|
+
}
|
|
2554
|
+
|
|
2555
|
+
// src/reachability/analyze.ts
|
|
2556
|
+
function analyzeReachability(repoRoot, target) {
|
|
2557
|
+
const report = analyzeBoundaries(repoRoot, target);
|
|
2558
|
+
return {
|
|
2559
|
+
deadEnds: report.deadEnds,
|
|
2560
|
+
deadSurfaces: report.deadSurfaces,
|
|
2561
|
+
files: report.files,
|
|
2562
|
+
target: report.target
|
|
2563
|
+
};
|
|
2564
|
+
}
|
|
2565
|
+
|
|
2566
|
+
// src/reachability/report.ts
|
|
2567
|
+
function logLines3(lines) {
|
|
2568
|
+
for (const line of lines) {
|
|
2569
|
+
console.log(line);
|
|
2570
|
+
}
|
|
2571
|
+
}
|
|
2572
|
+
function summaryLines3(report) {
|
|
2573
|
+
return [
|
|
2574
|
+
"",
|
|
2575
|
+
`Reachability for ${report.target}`,
|
|
2576
|
+
"\u2501".repeat(72),
|
|
2577
|
+
`Files: ${report.files.length}`,
|
|
2578
|
+
`Dead surfaces: ${report.deadSurfaces.length}`,
|
|
2579
|
+
`Dead ends: ${report.deadEnds.length}`,
|
|
2580
|
+
""
|
|
2581
|
+
];
|
|
2582
|
+
}
|
|
2583
|
+
function formatFile(file) {
|
|
2584
|
+
const layerLabel = file.layer ? ` [${file.layer}]` : "";
|
|
2585
|
+
return `- ${file.relativePath}${layerLabel} (in: ${file.incoming}, out: ${file.outgoing})`;
|
|
2586
|
+
}
|
|
2587
|
+
function reportReachability(report, options = {}) {
|
|
2588
|
+
if (report.files.length === 0) {
|
|
2589
|
+
console.log("\nNo reachability-scope files found.\n");
|
|
2590
|
+
return;
|
|
2591
|
+
}
|
|
2592
|
+
logLines3(summaryLines3(report));
|
|
2593
|
+
if (report.deadSurfaces.length > 0) {
|
|
2594
|
+
console.log("Dead surfaces:");
|
|
2595
|
+
for (const file of report.deadSurfaces) {
|
|
2596
|
+
console.log(formatFile(file));
|
|
2597
|
+
}
|
|
2598
|
+
console.log("");
|
|
2599
|
+
}
|
|
2600
|
+
if (report.deadEnds.length > 0) {
|
|
2601
|
+
console.log("Dead ends:");
|
|
2602
|
+
for (const file of report.deadEnds) {
|
|
2603
|
+
console.log(formatFile(file));
|
|
2604
|
+
}
|
|
2605
|
+
console.log("");
|
|
2606
|
+
}
|
|
2607
|
+
if (options.verbose) {
|
|
2608
|
+
console.log("All analyzed files:");
|
|
2609
|
+
for (const file of report.files) {
|
|
2610
|
+
console.log(formatFile(file));
|
|
2611
|
+
}
|
|
2612
|
+
}
|
|
2613
|
+
}
|
|
2614
|
+
|
|
2615
|
+
// src/reachability/command.ts
|
|
2616
|
+
var DEFAULT_DEPENDENCIES4 = {
|
|
2617
|
+
analyzeReachability,
|
|
2618
|
+
reportReachability,
|
|
2619
|
+
resolveQualityTarget,
|
|
2620
|
+
setExitCode: (code) => {
|
|
2621
|
+
process.exitCode = code;
|
|
2622
|
+
}
|
|
2623
|
+
};
|
|
2624
|
+
function runReachabilityCli(rawArgs, dependencies = DEFAULT_DEPENDENCIES4) {
|
|
2625
|
+
const args2 = cleanCliArgs(rawArgs);
|
|
2626
|
+
const target = dependencies.resolveQualityTarget(
|
|
2627
|
+
REPO_ROOT,
|
|
2628
|
+
parseTargetArg(args2, [])
|
|
2629
|
+
);
|
|
2630
|
+
const report = dependencies.analyzeReachability(REPO_ROOT, target);
|
|
2631
|
+
const verbose = args2.includes("--verbose");
|
|
2632
|
+
const strict = args2.includes("--strict");
|
|
2633
|
+
const json = args2.includes("--json");
|
|
2634
|
+
if (json) {
|
|
2635
|
+
console.log(JSON.stringify(report, null, 2));
|
|
2636
|
+
} else {
|
|
2637
|
+
dependencies.reportReachability(report, { verbose });
|
|
2638
|
+
}
|
|
2639
|
+
const hasHardFailures = report.deadEnds.length > 0;
|
|
2640
|
+
const hasStrictFailures = strict && report.deadSurfaces.length > 0;
|
|
2641
|
+
if (hasHardFailures || hasStrictFailures) {
|
|
2642
|
+
dependencies.setExitCode(1);
|
|
2643
|
+
}
|
|
2644
|
+
}
|
|
2645
|
+
|
|
2646
|
+
// src/scrap/analysis/pipeline/run.ts
|
|
2647
|
+
import * as fs from "fs";
|
|
2648
|
+
import * as ts25 from "typescript";
|
|
2649
|
+
|
|
2650
|
+
// src/scrap/test/discovery/files.ts
|
|
2651
|
+
import { readFileSync as readFileSync10 } from "fs";
|
|
2652
|
+
|
|
2653
|
+
// src/scrap/test/discovery/globs.ts
|
|
2654
|
+
import { globSync as globSync2 } from "glob";
|
|
2655
|
+
function discoverPackageTestFiles(packageName, repoRoot) {
|
|
2656
|
+
const patterns = resolvePackageToolGlobs(repoRoot, packageName, "scrap");
|
|
2657
|
+
return [...new Set(patterns.include.flatMap((pattern) => globSync2(pattern, {
|
|
2658
|
+
absolute: true,
|
|
2659
|
+
cwd: repoRoot,
|
|
2660
|
+
ignore: patterns.exclude
|
|
2661
|
+
})))].sort();
|
|
2662
|
+
}
|
|
2663
|
+
|
|
2664
|
+
// src/scrap/test/discovery/packages.ts
|
|
2665
|
+
function packageNamesForTarget(target, repoRoot) {
|
|
2666
|
+
if (target.kind === "repo") {
|
|
2667
|
+
return listWorkspacePackages(repoRoot).map((workspacePackage) => workspacePackage.name);
|
|
2668
|
+
}
|
|
2669
|
+
return target.packageName ? [target.packageName] : [];
|
|
2670
|
+
}
|
|
2671
|
+
|
|
2672
|
+
// src/scrap/test/discovery/sourceScope.ts
|
|
2673
|
+
function packageRelativeRoot(target) {
|
|
2674
|
+
if (!target.packageRelativePath) {
|
|
2675
|
+
return void 0;
|
|
2676
|
+
}
|
|
2677
|
+
if (target.packageRelativePath === ".") {
|
|
2678
|
+
return target.relativePath;
|
|
2679
|
+
}
|
|
2680
|
+
if (target.relativePath === target.packageRelativePath) {
|
|
2681
|
+
return ".";
|
|
2682
|
+
}
|
|
2683
|
+
const suffix = `/${target.packageRelativePath}`;
|
|
2684
|
+
return target.relativePath.endsWith(suffix) ? target.relativePath.slice(0, -suffix.length) : void 0;
|
|
2685
|
+
}
|
|
2686
|
+
function packageTestRoot(target) {
|
|
2687
|
+
const relativeRoot = packageRelativeRoot(target);
|
|
2688
|
+
if (!relativeRoot) {
|
|
2689
|
+
return void 0;
|
|
2690
|
+
}
|
|
2691
|
+
return relativeRoot === "." ? "tests" : `${relativeRoot}/tests`;
|
|
2692
|
+
}
|
|
2693
|
+
function sourceTestScope(target) {
|
|
2694
|
+
if (!target.packageRoot || !target.packageRelativePath) {
|
|
2695
|
+
return void 0;
|
|
2696
|
+
}
|
|
2697
|
+
const testRoot = packageTestRoot(target);
|
|
2698
|
+
if (!testRoot) {
|
|
2699
|
+
return void 0;
|
|
2700
|
+
}
|
|
2701
|
+
if (target.packageRelativePath === "src") {
|
|
2702
|
+
return testRoot;
|
|
2703
|
+
}
|
|
2704
|
+
if (!target.packageRelativePath.startsWith("src/")) {
|
|
2705
|
+
return void 0;
|
|
2706
|
+
}
|
|
2707
|
+
return `${testRoot}/${target.packageRelativePath.slice("src/".length)}`;
|
|
2708
|
+
}
|
|
2709
|
+
|
|
2710
|
+
// src/scrap/test/discovery/path.ts
|
|
2711
|
+
function isTestPath(packageRelativePath) {
|
|
2712
|
+
if (!packageRelativePath) {
|
|
2713
|
+
return false;
|
|
2714
|
+
}
|
|
2715
|
+
return packageRelativePath === "tests" || packageRelativePath.startsWith("tests/");
|
|
2716
|
+
}
|
|
2717
|
+
|
|
2718
|
+
// src/scrap/test/discovery/explicitTarget.ts
|
|
2719
|
+
function hasExplicitTestFileTarget(target) {
|
|
2720
|
+
if (target.kind !== "file") {
|
|
2721
|
+
return false;
|
|
2722
|
+
}
|
|
2723
|
+
if (!target.packageName || !target.packageRelativePath) {
|
|
2724
|
+
return false;
|
|
2725
|
+
}
|
|
2726
|
+
return isTestPath(target.packageRelativePath);
|
|
2727
|
+
}
|
|
2728
|
+
|
|
2729
|
+
// src/scrap/test/discovery/targetScope.ts
|
|
2730
|
+
function isInsideTarget(target, repoRoot, absolutePath) {
|
|
2731
|
+
const relativePath = relativeTo(repoRoot, absolutePath);
|
|
2732
|
+
const mappedTestScope = sourceTestScope(target);
|
|
2733
|
+
if (mappedTestScope) {
|
|
2734
|
+
return relativePath === mappedTestScope || relativePath.startsWith(`${mappedTestScope}/`);
|
|
2735
|
+
}
|
|
2736
|
+
if (target.kind === "repo") {
|
|
2737
|
+
return true;
|
|
2738
|
+
}
|
|
2739
|
+
if (target.kind === "package") {
|
|
2740
|
+
if (target.relativePath === ".") {
|
|
2741
|
+
return relativePath !== "" && relativePath !== "." && relativePath !== ".." && !relativePath.startsWith("../");
|
|
2742
|
+
}
|
|
2743
|
+
return relativePath.startsWith(`${target.relativePath}/`);
|
|
2744
|
+
}
|
|
2745
|
+
return relativePath === target.relativePath || relativePath.startsWith(`${target.relativePath}/`);
|
|
2746
|
+
}
|
|
2747
|
+
|
|
2748
|
+
// src/scrap/test/discovery/files.ts
|
|
2749
|
+
function isBaselineMetricWithPath(metric) {
|
|
2750
|
+
return typeof metric.filePath === "string";
|
|
2751
|
+
}
|
|
2752
|
+
function discoverTestFiles(target) {
|
|
2753
|
+
if (hasExplicitTestFileTarget(target)) {
|
|
2754
|
+
return pathIncludedByTool(REPO_ROOT, target.packageName, "scrap", target.packageRelativePath) ? [target.absolutePath] : [];
|
|
2755
|
+
}
|
|
2756
|
+
return packageNamesForTarget(target, REPO_ROOT).flatMap((packageName) => discoverPackageTestFiles(packageName, REPO_ROOT)).filter((filePath) => isInsideTarget(target, REPO_ROOT, filePath));
|
|
2757
|
+
}
|
|
2758
|
+
function readBaselineMetrics(baselinePath) {
|
|
2759
|
+
return JSON.parse(readFileSync10(baselinePath, "utf-8"));
|
|
2760
|
+
}
|
|
2761
|
+
function baselineMetricsByPath2(baseline2) {
|
|
2762
|
+
return new Map(
|
|
2763
|
+
baseline2.filter(isBaselineMetricWithPath).map((metric) => [metric.filePath, metric])
|
|
2764
|
+
);
|
|
2765
|
+
}
|
|
2766
|
+
|
|
2767
|
+
// src/scrap/analysis/pipeline/actionability.ts
|
|
2768
|
+
function hasValidationIssues(metric) {
|
|
2769
|
+
return (metric.validationIssues?.length ?? 0) > 0;
|
|
2770
|
+
}
|
|
2771
|
+
function needsManualSplit(metric) {
|
|
2772
|
+
return metric.remediationMode === "SPLIT";
|
|
2773
|
+
}
|
|
2774
|
+
function shouldTableDrive(metric) {
|
|
2775
|
+
return (metric.recommendations ?? []).some((recommendation) => recommendation.kind === "TABLE_DRIVE") && (metric.extractionPressureScore ?? 0) === 0;
|
|
2776
|
+
}
|
|
2777
|
+
function shouldRefactorLocally(metric) {
|
|
2778
|
+
return metric.remediationMode === "LOCAL";
|
|
2779
|
+
}
|
|
2780
|
+
function aiActionability(metric) {
|
|
2781
|
+
if (hasValidationIssues(metric)) {
|
|
2782
|
+
return "REVIEW_FIRST";
|
|
2783
|
+
}
|
|
2784
|
+
if (needsManualSplit(metric)) {
|
|
2785
|
+
return "MANUAL_SPLIT";
|
|
2786
|
+
}
|
|
2787
|
+
if (shouldTableDrive(metric)) {
|
|
2788
|
+
return "AUTO_TABLE_DRIVE";
|
|
2789
|
+
}
|
|
2790
|
+
if (shouldRefactorLocally(metric)) {
|
|
2791
|
+
return "AUTO_REFACTOR";
|
|
2792
|
+
}
|
|
2793
|
+
return "LEAVE_ALONE";
|
|
2794
|
+
}
|
|
2795
|
+
|
|
2796
|
+
// src/scrap/example/calls/duplicates.ts
|
|
2797
|
+
function countedFingerprint(setup) {
|
|
2798
|
+
if (!setup.setupFingerprint || setup.setupLineCount < 2) {
|
|
2799
|
+
return void 0;
|
|
2800
|
+
}
|
|
2801
|
+
return setup.setupFingerprint;
|
|
2802
|
+
}
|
|
2803
|
+
function duplicateSetupGroupSizes(setups) {
|
|
2804
|
+
const counts = /* @__PURE__ */ new Map();
|
|
2805
|
+
for (const setup of setups) {
|
|
2806
|
+
const fingerprint = countedFingerprint(setup);
|
|
2807
|
+
if (fingerprint) {
|
|
2808
|
+
counts.set(fingerprint, (counts.get(fingerprint) ?? 0) + 1);
|
|
2809
|
+
}
|
|
2810
|
+
}
|
|
2811
|
+
return setups.map((setup) => {
|
|
2812
|
+
const fingerprint = countedFingerprint(setup);
|
|
2813
|
+
return fingerprint ? counts.get(fingerprint) ?? 0 : 0;
|
|
2814
|
+
});
|
|
2815
|
+
}
|
|
2816
|
+
function duplicateSetupExampleCount(groupSizes) {
|
|
2817
|
+
return groupSizes.filter((groupSize) => groupSize > 1).length;
|
|
2818
|
+
}
|
|
2819
|
+
|
|
2820
|
+
// src/scrap/example/calls/extract.ts
|
|
2821
|
+
import * as ts11 from "typescript";
|
|
2822
|
+
|
|
2823
|
+
// src/scrap/calls/names.ts
|
|
2824
|
+
import * as ts10 from "typescript";
|
|
2825
|
+
function callInfo(expression) {
|
|
2826
|
+
if (ts10.isIdentifier(expression)) {
|
|
2827
|
+
return { baseName: expression.text, tableDriven: false };
|
|
2828
|
+
}
|
|
2829
|
+
if (ts10.isPropertyAccessExpression(expression)) {
|
|
2830
|
+
const parent = callInfo(expression.expression);
|
|
2831
|
+
return {
|
|
2832
|
+
baseName: parent.baseName,
|
|
2833
|
+
tableDriven: parent.tableDriven || expression.name.text === "each"
|
|
2834
|
+
};
|
|
2835
|
+
}
|
|
2836
|
+
if (ts10.isCallExpression(expression)) {
|
|
2837
|
+
return callInfo(expression.expression);
|
|
2838
|
+
}
|
|
2839
|
+
return { baseName: void 0, tableDriven: false };
|
|
2840
|
+
}
|
|
2841
|
+
function baseCallName(expression) {
|
|
2842
|
+
return callInfo(expression).baseName;
|
|
2843
|
+
}
|
|
2844
|
+
function terminalCallName(expression) {
|
|
2845
|
+
if (ts10.isIdentifier(expression)) {
|
|
2846
|
+
return expression.text;
|
|
2847
|
+
}
|
|
2848
|
+
if (ts10.isPropertyAccessExpression(expression)) {
|
|
2849
|
+
return expression.name.text;
|
|
2850
|
+
}
|
|
2851
|
+
if (ts10.isCallExpression(expression)) {
|
|
2852
|
+
return terminalCallName(expression.expression);
|
|
2853
|
+
}
|
|
2854
|
+
return void 0;
|
|
2855
|
+
}
|
|
2856
|
+
function literalName(node) {
|
|
2857
|
+
return ts10.isStringLiteralLike(node) ? node.text : "(anonymous)";
|
|
2858
|
+
}
|
|
2859
|
+
function callbackArgument(node) {
|
|
2860
|
+
return node.arguments.find((argument) => ts10.isArrowFunction(argument) || ts10.isFunctionExpression(argument));
|
|
2861
|
+
}
|
|
2862
|
+
|
|
2863
|
+
// src/scrap/example/calls/extract.ts
|
|
2864
|
+
var BRANCHING_KINDS2 = /* @__PURE__ */ new Set([
|
|
2865
|
+
ts11.SyntaxKind.IfStatement,
|
|
2866
|
+
ts11.SyntaxKind.ForStatement,
|
|
2867
|
+
ts11.SyntaxKind.ForInStatement,
|
|
2868
|
+
ts11.SyntaxKind.ForOfStatement,
|
|
2869
|
+
ts11.SyntaxKind.WhileStatement,
|
|
2870
|
+
ts11.SyntaxKind.DoStatement,
|
|
2871
|
+
ts11.SyntaxKind.SwitchStatement,
|
|
2872
|
+
ts11.SyntaxKind.ConditionalExpression
|
|
2873
|
+
]);
|
|
2874
|
+
function isExpectCall(node) {
|
|
2875
|
+
return ts11.isIdentifier(node.expression) && node.expression.text === "expect";
|
|
2876
|
+
}
|
|
2877
|
+
function isTypeOnlyAssertionCall(node) {
|
|
2878
|
+
const terminal = terminalCallName(node.expression);
|
|
2879
|
+
return terminal === "assertType" || ts11.isPropertyAccessExpression(node.expression) && baseCallName(node.expression) === "expectTypeOf";
|
|
2880
|
+
}
|
|
2881
|
+
function isAssertionCall(node) {
|
|
2882
|
+
return isExpectCall(node) || isTypeOnlyAssertionCall(node);
|
|
2883
|
+
}
|
|
2884
|
+
function isMockCall(node) {
|
|
2885
|
+
return ts11.isPropertyAccessExpression(node.expression) && ts11.isIdentifier(node.expression.expression) && ["vi", "jest"].includes(node.expression.expression.text) && ["mock", "spyOn"].includes(node.expression.name.text);
|
|
2886
|
+
}
|
|
2887
|
+
function countBranches(node) {
|
|
2888
|
+
let total = BRANCHING_KINDS2.has(node.kind) ? 1 : 0;
|
|
2889
|
+
ts11.forEachChild(node, (child) => {
|
|
2890
|
+
total += countBranches(child);
|
|
2891
|
+
});
|
|
2892
|
+
return total;
|
|
2893
|
+
}
|
|
2894
|
+
function collectCallCount(node, matcher) {
|
|
2895
|
+
let count = 0;
|
|
2896
|
+
function walk2(current) {
|
|
2897
|
+
if (ts11.isCallExpression(current) && matcher(current)) {
|
|
2898
|
+
count++;
|
|
2899
|
+
}
|
|
2900
|
+
ts11.forEachChild(current, walk2);
|
|
2901
|
+
}
|
|
2902
|
+
walk2(node);
|
|
2903
|
+
return count;
|
|
2904
|
+
}
|
|
2905
|
+
|
|
2906
|
+
// src/scrap/example/setup.ts
|
|
2907
|
+
import * as ts13 from "typescript";
|
|
2908
|
+
|
|
2909
|
+
// src/scrap/calls/normalizedShapes.ts
|
|
2910
|
+
import * as ts12 from "typescript";
|
|
2911
|
+
var NORMALIZED_LITERAL_KINDS = /* @__PURE__ */ new Set([
|
|
2912
|
+
ts12.SyntaxKind.StringLiteral,
|
|
2913
|
+
ts12.SyntaxKind.NoSubstitutionTemplateLiteral,
|
|
2914
|
+
ts12.SyntaxKind.NumericLiteral,
|
|
2915
|
+
ts12.SyntaxKind.TrueKeyword,
|
|
2916
|
+
ts12.SyntaxKind.FalseKeyword,
|
|
2917
|
+
ts12.SyntaxKind.NullKeyword
|
|
2918
|
+
]);
|
|
2919
|
+
function isNormalizedLiteralKind(kind) {
|
|
2920
|
+
return NORMALIZED_LITERAL_KINDS.has(kind);
|
|
2921
|
+
}
|
|
2922
|
+
function normalizedLeafFingerprint(node) {
|
|
2923
|
+
if (ts12.isIdentifier(node)) {
|
|
2924
|
+
return "id";
|
|
2925
|
+
}
|
|
2926
|
+
if (isNormalizedLiteralKind(node.kind)) {
|
|
2927
|
+
return "lit";
|
|
2928
|
+
}
|
|
2929
|
+
return void 0;
|
|
2930
|
+
}
|
|
2931
|
+
function literalShapeLeafFingerprint(node) {
|
|
2932
|
+
if (ts12.isIdentifier(node)) {
|
|
2933
|
+
return node.text;
|
|
2934
|
+
}
|
|
2935
|
+
if (isNormalizedLiteralKind(node.kind)) {
|
|
2936
|
+
return "lit";
|
|
2937
|
+
}
|
|
2938
|
+
return void 0;
|
|
2939
|
+
}
|
|
2940
|
+
function fingerprintChildren(node, serializer) {
|
|
2941
|
+
const children = [];
|
|
2942
|
+
ts12.forEachChild(node, (child) => {
|
|
2943
|
+
children.push(serializer(child));
|
|
2944
|
+
});
|
|
2945
|
+
return children;
|
|
2946
|
+
}
|
|
2947
|
+
function fingerprintNodeWithLeaf(node, leafFingerprint) {
|
|
2948
|
+
const leaf = leafFingerprint(node);
|
|
2949
|
+
if (leaf) {
|
|
2950
|
+
return leaf;
|
|
2951
|
+
}
|
|
2952
|
+
return `${node.kind}[${fingerprintChildren(node, (child) => fingerprintNodeWithLeaf(child, leafFingerprint)).join(",")}]`;
|
|
2953
|
+
}
|
|
2954
|
+
function fingerprintNode(node) {
|
|
2955
|
+
return fingerprintNodeWithLeaf(node, normalizedLeafFingerprint);
|
|
2956
|
+
}
|
|
2957
|
+
function collectFeatures(node, features, serializer) {
|
|
2958
|
+
features.add(serializer(node));
|
|
2959
|
+
ts12.forEachChild(node, (child) => collectFeatures(child, features, serializer));
|
|
2960
|
+
}
|
|
2961
|
+
function statementFingerprintWithSerializer(statements, serializer) {
|
|
2962
|
+
if (statements.length === 0) {
|
|
2963
|
+
return void 0;
|
|
2964
|
+
}
|
|
2965
|
+
return statements.map((statement) => serializer(statement)).join("|");
|
|
2966
|
+
}
|
|
2967
|
+
function statementFingerprint(statements) {
|
|
2968
|
+
return statementFingerprintWithSerializer(statements, fingerprintNode);
|
|
2969
|
+
}
|
|
2970
|
+
function literalShapeFingerprint(statements) {
|
|
2971
|
+
return statementFingerprintWithSerializer(
|
|
2972
|
+
statements,
|
|
2973
|
+
(statement) => fingerprintNodeWithLeaf(statement, literalShapeLeafFingerprint)
|
|
2974
|
+
);
|
|
2975
|
+
}
|
|
2976
|
+
function statementFeatures(statements) {
|
|
2977
|
+
const features = /* @__PURE__ */ new Set();
|
|
2978
|
+
statements.forEach((statement) => collectFeatures(statement, features, fingerprintNode));
|
|
2979
|
+
return [...features].sort();
|
|
2980
|
+
}
|
|
2981
|
+
|
|
2982
|
+
// src/scrap/example/setup.ts
|
|
2983
|
+
function statementLineCount(sourceFile, statement) {
|
|
2984
|
+
const start = sourceFile.getLineAndCharacterOfPosition(statement.getStart(sourceFile));
|
|
2985
|
+
const end = sourceFile.getLineAndCharacterOfPosition(statement.getEnd());
|
|
2986
|
+
return end.line - start.line + 1;
|
|
2987
|
+
}
|
|
2988
|
+
function bodyStatements(example) {
|
|
2989
|
+
const body = example.body.body;
|
|
2990
|
+
if (!body || !ts13.isBlock(body)) {
|
|
2991
|
+
return [];
|
|
2992
|
+
}
|
|
2993
|
+
return [...body.statements];
|
|
2994
|
+
}
|
|
2995
|
+
function setupStatements(example) {
|
|
2996
|
+
const statements = [];
|
|
2997
|
+
for (const statement of bodyStatements(example)) {
|
|
2998
|
+
if (collectCallCount(statement, isAssertionCall) > 0) {
|
|
2999
|
+
break;
|
|
3000
|
+
}
|
|
3001
|
+
statements.push(statement);
|
|
3002
|
+
}
|
|
3003
|
+
return statements;
|
|
3004
|
+
}
|
|
3005
|
+
function assertionStatements(example) {
|
|
3006
|
+
const statements = bodyStatements(example);
|
|
3007
|
+
const firstAssertionIndex = statements.findIndex((statement) => collectCallCount(statement, isAssertionCall) > 0);
|
|
3008
|
+
if (firstAssertionIndex === -1) {
|
|
3009
|
+
return [];
|
|
3010
|
+
}
|
|
3011
|
+
return statements.slice(firstAssertionIndex);
|
|
3012
|
+
}
|
|
3013
|
+
function allExampleStatements(example) {
|
|
3014
|
+
return bodyStatements(example);
|
|
3015
|
+
}
|
|
3016
|
+
function analyzeExampleSetup(sourceFile, example) {
|
|
3017
|
+
const statements = setupStatements(example);
|
|
3018
|
+
return {
|
|
3019
|
+
setupFingerprint: statementFingerprint(statements),
|
|
3020
|
+
setupLineCount: statements.reduce(
|
|
3021
|
+
(total, statement) => total + statementLineCount(sourceFile, statement),
|
|
3022
|
+
0
|
|
3023
|
+
)
|
|
3024
|
+
};
|
|
3025
|
+
}
|
|
3026
|
+
|
|
3027
|
+
// src/scrap/example/signals.ts
|
|
3028
|
+
import * as ts17 from "typescript";
|
|
3029
|
+
|
|
3030
|
+
// src/scrap/analysis/vitestSignals.ts
|
|
3031
|
+
import * as ts16 from "typescript";
|
|
3032
|
+
|
|
3033
|
+
// src/scrap/vitest/asyncMatchers.ts
|
|
3034
|
+
import * as ts15 from "typescript";
|
|
3035
|
+
|
|
3036
|
+
// src/scrap/calls/propertyMatcher.ts
|
|
3037
|
+
import * as ts14 from "typescript";
|
|
3038
|
+
function hasPropertyName(expression, name) {
|
|
3039
|
+
if (ts14.isCallExpression(expression)) {
|
|
3040
|
+
return hasPropertyName(expression.expression, name);
|
|
3041
|
+
}
|
|
3042
|
+
if (!ts14.isPropertyAccessExpression(expression)) {
|
|
3043
|
+
return false;
|
|
3044
|
+
}
|
|
3045
|
+
return expression.name.text === name || hasPropertyName(expression.expression, name);
|
|
3046
|
+
}
|
|
3047
|
+
function matchesTerminalName(expression, names) {
|
|
3048
|
+
const name = terminalCallName(expression);
|
|
3049
|
+
if (name === void 0) {
|
|
3050
|
+
return false;
|
|
3051
|
+
}
|
|
3052
|
+
return names.has(name);
|
|
3053
|
+
}
|
|
3054
|
+
|
|
3055
|
+
// src/scrap/vitest/asyncMatchers.ts
|
|
3056
|
+
var ASYNC_WAIT_CALLS = /* @__PURE__ */ new Set([
|
|
3057
|
+
"waitFor",
|
|
3058
|
+
"waitForElementToBeRemoved"
|
|
3059
|
+
]);
|
|
3060
|
+
var CONCURRENT_BASE_CALLS = /* @__PURE__ */ new Set([
|
|
3061
|
+
"describe",
|
|
3062
|
+
"it",
|
|
3063
|
+
"test"
|
|
3064
|
+
]);
|
|
3065
|
+
function isAsyncWaitCall(node) {
|
|
3066
|
+
const terminal = terminalCallName(node.expression);
|
|
3067
|
+
return terminal !== void 0 && (ASYNC_WAIT_CALLS.has(terminal) || terminal.startsWith("findBy") || terminal.startsWith("findAllBy"));
|
|
3068
|
+
}
|
|
3069
|
+
function isConcurrencyCall(node) {
|
|
3070
|
+
return ts15.isPropertyAccessExpression(node.expression) && hasPropertyName(node.expression, "concurrent") && CONCURRENT_BASE_CALLS.has(baseCallName(node.expression) ?? "");
|
|
3071
|
+
}
|
|
3072
|
+
|
|
3073
|
+
// src/scrap/vitest/mutationMatchers.ts
|
|
3074
|
+
var ENV_MUTATION_CALLS = /* @__PURE__ */ new Set([
|
|
3075
|
+
"stubEnv",
|
|
3076
|
+
"stubGlobal",
|
|
3077
|
+
"unstubAllEnvs",
|
|
3078
|
+
"unstubAllGlobals"
|
|
3079
|
+
]);
|
|
3080
|
+
var FAKE_TIMER_CALLS = /* @__PURE__ */ new Set([
|
|
3081
|
+
"setSystemTime",
|
|
3082
|
+
"useFakeTimers",
|
|
3083
|
+
"useRealTimers"
|
|
3084
|
+
]);
|
|
3085
|
+
var MODULE_MOCK_CALLS = /* @__PURE__ */ new Set([
|
|
3086
|
+
"doMock",
|
|
3087
|
+
"doUnmock",
|
|
3088
|
+
"hoisted",
|
|
3089
|
+
"importActual",
|
|
3090
|
+
"importMock",
|
|
3091
|
+
"mocked",
|
|
3092
|
+
"unmock"
|
|
3093
|
+
]);
|
|
3094
|
+
var SNAPSHOT_CALLS = /* @__PURE__ */ new Set([
|
|
3095
|
+
"toMatchInlineSnapshot",
|
|
3096
|
+
"toMatchSnapshot"
|
|
3097
|
+
]);
|
|
3098
|
+
function isEnvironmentMutationCall(node) {
|
|
3099
|
+
return baseCallName(node.expression) === "vi" && matchesTerminalName(node.expression, ENV_MUTATION_CALLS);
|
|
3100
|
+
}
|
|
3101
|
+
function isFakeTimerMutationCall(node) {
|
|
3102
|
+
return baseCallName(node.expression) === "vi" && matchesTerminalName(node.expression, FAKE_TIMER_CALLS);
|
|
3103
|
+
}
|
|
3104
|
+
function isModuleMockLifecycleCall(node) {
|
|
3105
|
+
return baseCallName(node.expression) === "vi" && matchesTerminalName(node.expression, MODULE_MOCK_CALLS);
|
|
3106
|
+
}
|
|
3107
|
+
function isSnapshotCall(node) {
|
|
3108
|
+
return matchesTerminalName(node.expression, SNAPSHOT_CALLS);
|
|
3109
|
+
}
|
|
3110
|
+
|
|
3111
|
+
// src/scrap/analysis/vitestSignals.ts
|
|
3112
|
+
function countConcurrentAncestors(node) {
|
|
3113
|
+
let count = 0;
|
|
3114
|
+
let parent = node.parent;
|
|
3115
|
+
while (parent) {
|
|
3116
|
+
if (ts16.isCallExpression(parent) && isConcurrencyCall(parent)) {
|
|
3117
|
+
count += 1;
|
|
3118
|
+
}
|
|
3119
|
+
parent = parent.parent;
|
|
3120
|
+
}
|
|
3121
|
+
return count;
|
|
3122
|
+
}
|
|
3123
|
+
function analyzeVitestSignals(node) {
|
|
3124
|
+
return {
|
|
3125
|
+
asyncWaitCount: collectCallCount(node, isAsyncWaitCall),
|
|
3126
|
+
concurrencyCount: collectCallCount(node, isConcurrencyCall) + countConcurrentAncestors(node),
|
|
3127
|
+
envMutationCount: collectCallCount(node, isEnvironmentMutationCall),
|
|
3128
|
+
fakeTimerCount: collectCallCount(node, isFakeTimerMutationCall),
|
|
3129
|
+
moduleMockCount: collectCallCount(node, isModuleMockLifecycleCall),
|
|
3130
|
+
snapshotCount: collectCallCount(node, isSnapshotCall),
|
|
3131
|
+
typeOnlyAssertionCount: collectCallCount(node, isTypeOnlyAssertionCall)
|
|
3132
|
+
};
|
|
3133
|
+
}
|
|
3134
|
+
|
|
3135
|
+
// src/scrap/calls/resources.ts
|
|
3136
|
+
var TEMP_RESOURCE_CALLS = /* @__PURE__ */ new Set([
|
|
3137
|
+
"mkdtemp",
|
|
3138
|
+
"mkdtempSync",
|
|
3139
|
+
"mkdir",
|
|
3140
|
+
"mkdirSync",
|
|
3141
|
+
"tmpdir",
|
|
3142
|
+
"writeFile",
|
|
3143
|
+
"writeFileSync"
|
|
3144
|
+
]);
|
|
3145
|
+
function isTempResourceCallName(callName) {
|
|
3146
|
+
return TEMP_RESOURCE_CALLS.has(callName);
|
|
3147
|
+
}
|
|
3148
|
+
|
|
3149
|
+
// src/scrap/example/signals.ts
|
|
3150
|
+
var DEPTH_KINDS = /* @__PURE__ */ new Set([
|
|
3151
|
+
ts17.SyntaxKind.IfStatement,
|
|
3152
|
+
ts17.SyntaxKind.ForStatement,
|
|
3153
|
+
ts17.SyntaxKind.ForInStatement,
|
|
3154
|
+
ts17.SyntaxKind.ForOfStatement,
|
|
3155
|
+
ts17.SyntaxKind.WhileStatement,
|
|
3156
|
+
ts17.SyntaxKind.DoStatement,
|
|
3157
|
+
ts17.SyntaxKind.SwitchStatement,
|
|
3158
|
+
ts17.SyntaxKind.TryStatement,
|
|
3159
|
+
ts17.SyntaxKind.ConditionalExpression
|
|
3160
|
+
]);
|
|
3161
|
+
function isBranchNode(node) {
|
|
3162
|
+
return DEPTH_KINDS.has(node.kind);
|
|
3163
|
+
}
|
|
3164
|
+
function maxSetupDepth(node, depth = 0) {
|
|
3165
|
+
const branchDepth = isBranchNode(node) ? depth + 1 : depth;
|
|
3166
|
+
let maxDepth = branchDepth;
|
|
3167
|
+
ts17.forEachChild(node, (child) => {
|
|
3168
|
+
maxDepth = Math.max(maxDepth, maxSetupDepth(child, branchDepth));
|
|
3169
|
+
});
|
|
3170
|
+
return maxDepth;
|
|
3171
|
+
}
|
|
3172
|
+
function countTempResourceWork(node) {
|
|
3173
|
+
let count = 0;
|
|
3174
|
+
function walk2(current) {
|
|
3175
|
+
if (ts17.isCallExpression(current)) {
|
|
3176
|
+
const callName = terminalCallName(current.expression);
|
|
3177
|
+
if (isTempResourceCallName(callName)) {
|
|
3178
|
+
count += 1;
|
|
3179
|
+
}
|
|
3180
|
+
}
|
|
3181
|
+
ts17.forEachChild(current, walk2);
|
|
3182
|
+
}
|
|
3183
|
+
walk2(node);
|
|
3184
|
+
return count;
|
|
3185
|
+
}
|
|
3186
|
+
|
|
3187
|
+
// src/scrap/calls/fixture.ts
|
|
3188
|
+
import * as ts18 from "typescript";
|
|
3189
|
+
function hasFixtureCall(node) {
|
|
3190
|
+
let found = false;
|
|
3191
|
+
function walk2(current) {
|
|
3192
|
+
if (ts18.isCallExpression(current)) {
|
|
3193
|
+
const callName = terminalCallName(current.expression);
|
|
3194
|
+
if (isTempResourceCallName(callName)) {
|
|
3195
|
+
found = true;
|
|
3196
|
+
return;
|
|
3197
|
+
}
|
|
3198
|
+
}
|
|
3199
|
+
ts18.forEachChild(current, walk2);
|
|
3200
|
+
}
|
|
3201
|
+
walk2(node);
|
|
3202
|
+
return found;
|
|
3203
|
+
}
|
|
3204
|
+
function fixtureStatements(node) {
|
|
3205
|
+
const statements = [];
|
|
3206
|
+
ts18.forEachChild(node, (child) => {
|
|
3207
|
+
if (ts18.isStatement(child) && hasFixtureCall(child)) {
|
|
3208
|
+
statements.push(child);
|
|
3209
|
+
}
|
|
3210
|
+
if (ts18.isFunctionLike(child)) {
|
|
3211
|
+
statements.push(...fixtureStatements(child));
|
|
3212
|
+
return;
|
|
3213
|
+
}
|
|
3214
|
+
if (ts18.isBlock(child)) {
|
|
3215
|
+
statements.push(...fixtureStatements(child));
|
|
3216
|
+
}
|
|
3217
|
+
});
|
|
3218
|
+
return statements;
|
|
3219
|
+
}
|
|
3220
|
+
|
|
3221
|
+
// src/scrap/structure/helpers/definitions.ts
|
|
3222
|
+
import * as ts20 from "typescript";
|
|
3223
|
+
|
|
3224
|
+
// src/scrap/structure/helpers/containers.ts
|
|
3225
|
+
import * as ts19 from "typescript";
|
|
3226
|
+
function findHelperContainer(node) {
|
|
3227
|
+
for (let current = node; current; current = current.parent) {
|
|
3228
|
+
if (ts19.isBlock(current) || ts19.isSourceFile(current)) {
|
|
3229
|
+
return current;
|
|
3230
|
+
}
|
|
3231
|
+
}
|
|
3232
|
+
return void 0;
|
|
3233
|
+
}
|
|
3234
|
+
function ancestorHelperContainers(node) {
|
|
3235
|
+
const containers = [];
|
|
3236
|
+
for (let current = findHelperContainer(node.parent); current; current = findHelperContainer(current.parent)) {
|
|
3237
|
+
containers.push(current);
|
|
3238
|
+
}
|
|
3239
|
+
return containers;
|
|
3240
|
+
}
|
|
3241
|
+
|
|
3242
|
+
// src/scrap/structure/helpers/definitions.ts
|
|
3243
|
+
function helperLineCount(sourceFile, helper) {
|
|
3244
|
+
const start = sourceFile.getLineAndCharacterOfPosition(helper.getStart());
|
|
3245
|
+
const end = sourceFile.getLineAndCharacterOfPosition(helper.getEnd());
|
|
3246
|
+
return end.line - start.line + 1;
|
|
3247
|
+
}
|
|
3248
|
+
function createHelperDefinition(sourceFile, name, helper, container) {
|
|
3249
|
+
return {
|
|
3250
|
+
body: helper,
|
|
3251
|
+
container,
|
|
3252
|
+
key: `${name}:${helper.getStart()}:${helper.getEnd()}`,
|
|
3253
|
+
lineCount: helperLineCount(sourceFile, helper),
|
|
3254
|
+
name
|
|
3255
|
+
};
|
|
3256
|
+
}
|
|
3257
|
+
function functionDeclarationDefinition(sourceFile, node) {
|
|
3258
|
+
if (!node.name || !node.body) {
|
|
3259
|
+
return void 0;
|
|
3260
|
+
}
|
|
3261
|
+
const container = findHelperContainer(node.parent);
|
|
3262
|
+
if (!container) {
|
|
3263
|
+
return void 0;
|
|
3264
|
+
}
|
|
3265
|
+
return createHelperDefinition(sourceFile, node.name.text, node, container);
|
|
3266
|
+
}
|
|
3267
|
+
function functionInitializer(declaration) {
|
|
3268
|
+
const { initializer } = declaration;
|
|
3269
|
+
if (initializer && (ts20.isArrowFunction(initializer) || ts20.isFunctionExpression(initializer))) {
|
|
3270
|
+
return initializer;
|
|
3271
|
+
}
|
|
3272
|
+
return void 0;
|
|
3273
|
+
}
|
|
3274
|
+
function variableDeclarationDefinition(sourceFile, node) {
|
|
3275
|
+
if (!ts20.isIdentifier(node.name)) {
|
|
3276
|
+
return void 0;
|
|
3277
|
+
}
|
|
3278
|
+
const initializer = functionInitializer(node);
|
|
3279
|
+
const container = findHelperContainer(node.parent);
|
|
3280
|
+
if (!initializer || !container) {
|
|
3281
|
+
return void 0;
|
|
3282
|
+
}
|
|
3283
|
+
return createHelperDefinition(sourceFile, node.name.text, initializer, container);
|
|
3284
|
+
}
|
|
3285
|
+
function collectDefinition(sourceFile, node) {
|
|
3286
|
+
if (ts20.isFunctionDeclaration(node)) {
|
|
3287
|
+
return functionDeclarationDefinition(sourceFile, node);
|
|
3288
|
+
}
|
|
3289
|
+
if (ts20.isVariableDeclaration(node)) {
|
|
3290
|
+
return variableDeclarationDefinition(sourceFile, node);
|
|
3291
|
+
}
|
|
3292
|
+
return void 0;
|
|
3293
|
+
}
|
|
3294
|
+
function collectHelperDefinitions(sourceFile) {
|
|
3295
|
+
const definitions = [];
|
|
3296
|
+
function walk2(node) {
|
|
3297
|
+
const definition = collectDefinition(sourceFile, node);
|
|
3298
|
+
if (definition) {
|
|
3299
|
+
definitions.push(definition);
|
|
3300
|
+
}
|
|
3301
|
+
ts20.forEachChild(node, walk2);
|
|
3302
|
+
}
|
|
3303
|
+
walk2(sourceFile);
|
|
3304
|
+
return definitions;
|
|
3305
|
+
}
|
|
3306
|
+
|
|
3307
|
+
// src/scrap/structure/helpers/reachability.ts
|
|
3308
|
+
import * as ts21 from "typescript";
|
|
3309
|
+
function helpersInContainer(container, helpers) {
|
|
3310
|
+
return helpers.filter((helper) => helper.container === container);
|
|
3311
|
+
}
|
|
3312
|
+
function visibleHelpers(scopeNode, helpers) {
|
|
3313
|
+
const visible = /* @__PURE__ */ new Map();
|
|
3314
|
+
for (const container of ancestorHelperContainers(scopeNode)) {
|
|
3315
|
+
for (const helper of helpersInContainer(container, helpers)) {
|
|
3316
|
+
if (!visible.has(helper.name)) {
|
|
3317
|
+
visible.set(helper.name, helper);
|
|
3318
|
+
}
|
|
3319
|
+
}
|
|
3320
|
+
}
|
|
3321
|
+
return visible;
|
|
3322
|
+
}
|
|
3323
|
+
function directHelperCalls(scopeNode, helpers) {
|
|
3324
|
+
const body = scopeNode.body;
|
|
3325
|
+
if (!body) {
|
|
3326
|
+
return [];
|
|
3327
|
+
}
|
|
3328
|
+
const calls = [];
|
|
3329
|
+
const visible = visibleHelpers(scopeNode, helpers);
|
|
3330
|
+
function visitCall(node) {
|
|
3331
|
+
if (ts21.isCallExpression(node) && ts21.isIdentifier(node.expression)) {
|
|
3332
|
+
const helper = visible.get(node.expression.text);
|
|
3333
|
+
if (helper) {
|
|
3334
|
+
calls.push(helper);
|
|
3335
|
+
}
|
|
3336
|
+
}
|
|
3337
|
+
ts21.forEachChild(node, visitCall);
|
|
3338
|
+
}
|
|
3339
|
+
visitCall(body);
|
|
3340
|
+
return calls;
|
|
3341
|
+
}
|
|
3342
|
+
function appendReachableHelpers(helper, helpers, visited, reachable) {
|
|
3343
|
+
if (visited.has(helper.key)) {
|
|
3344
|
+
return;
|
|
3345
|
+
}
|
|
3346
|
+
visited.add(helper.key);
|
|
3347
|
+
reachable.push(helper);
|
|
3348
|
+
for (const nestedHelper of directHelperCalls(helper.body, helpers)) {
|
|
3349
|
+
appendReachableHelpers(nestedHelper, helpers, visited, reachable);
|
|
3350
|
+
}
|
|
3351
|
+
}
|
|
3352
|
+
function reachableHelpers(scopeNode, helpers, visited = /* @__PURE__ */ new Set()) {
|
|
3353
|
+
const reachable = [];
|
|
3354
|
+
for (const helper of directHelperCalls(scopeNode, helpers)) {
|
|
3355
|
+
appendReachableHelpers(helper, helpers, visited, reachable);
|
|
3356
|
+
}
|
|
3357
|
+
return reachable;
|
|
3358
|
+
}
|
|
3359
|
+
|
|
3360
|
+
// src/scrap/structure/helpers/usage.ts
|
|
3361
|
+
function analyzeHelperUsage(sourceFile, example) {
|
|
3362
|
+
const helpers = collectHelperDefinitions(sourceFile);
|
|
3363
|
+
const directCalls = directHelperCalls(example.body, helpers);
|
|
3364
|
+
const hiddenHelpers = reachableHelpers(example.body, helpers, /* @__PURE__ */ new Set());
|
|
3365
|
+
return {
|
|
3366
|
+
helperCallCount: directCalls.length,
|
|
3367
|
+
helperHiddenLineCount: hiddenHelpers.reduce((total, helper) => total + helper.lineCount, 0)
|
|
3368
|
+
};
|
|
3369
|
+
}
|
|
3370
|
+
|
|
3371
|
+
// src/scrap/analysis/rtlSignals.ts
|
|
3372
|
+
var QUERY_PREFIXES = ["findAllBy", "findBy", "getAllBy", "getBy", "queryAllBy", "queryBy"];
|
|
3373
|
+
var RENDER_CALLS = /* @__PURE__ */ new Set([
|
|
3374
|
+
"render",
|
|
3375
|
+
"renderHook",
|
|
3376
|
+
"rerender"
|
|
3377
|
+
]);
|
|
3378
|
+
var MUTATION_BASE_CALLS = /* @__PURE__ */ new Set([
|
|
3379
|
+
"fireEvent",
|
|
3380
|
+
"userEvent"
|
|
3381
|
+
]);
|
|
3382
|
+
function hasQueryPrefix(terminal) {
|
|
3383
|
+
return QUERY_PREFIXES.some((prefix) => terminal?.startsWith(prefix) ?? false);
|
|
3384
|
+
}
|
|
3385
|
+
function isRtlQueryCall(node) {
|
|
3386
|
+
return hasQueryPrefix(terminalCallName(node.expression));
|
|
3387
|
+
}
|
|
3388
|
+
function isRtlRenderCall(node) {
|
|
3389
|
+
return RENDER_CALLS.has(terminalCallName(node.expression) ?? "");
|
|
3390
|
+
}
|
|
3391
|
+
function isRtlMutationCall(node) {
|
|
3392
|
+
return MUTATION_BASE_CALLS.has(baseCallName(node.expression) ?? "") || terminalCallName(node.expression) === "rerender";
|
|
3393
|
+
}
|
|
3394
|
+
function analyzeRtlSignals(node) {
|
|
3395
|
+
return {
|
|
3396
|
+
rtlMutationCount: collectCallCount(node, isRtlMutationCall),
|
|
3397
|
+
rtlQueryCount: collectCallCount(node, isRtlQueryCall),
|
|
3398
|
+
rtlRenderCount: collectCallCount(node, isRtlRenderCall)
|
|
3399
|
+
};
|
|
3400
|
+
}
|
|
3401
|
+
|
|
3402
|
+
// src/scrap/example/calls/pressure.ts
|
|
3403
|
+
function linePressure(lineCount) {
|
|
3404
|
+
return Math.max(0, Math.min(6, Math.ceil((lineCount - 8) / 6)));
|
|
3405
|
+
}
|
|
3406
|
+
function assertionPressure(assertionCount) {
|
|
3407
|
+
if (assertionCount === 0) {
|
|
3408
|
+
return 8;
|
|
3409
|
+
}
|
|
3410
|
+
return assertionCount === 1 ? 3 : 0;
|
|
3411
|
+
}
|
|
3412
|
+
function branchPressure(branchCount) {
|
|
3413
|
+
return Math.min(6, branchCount * 2);
|
|
3414
|
+
}
|
|
3415
|
+
function mockPressure(mockCount) {
|
|
3416
|
+
return Math.min(4, mockCount);
|
|
3417
|
+
}
|
|
3418
|
+
function helperHiddenPressure(helperHiddenLineCount) {
|
|
3419
|
+
return Math.max(0, Math.min(6, Math.ceil((helperHiddenLineCount - 4) / 6)));
|
|
3420
|
+
}
|
|
3421
|
+
function duplicateSetupPressure(duplicateSetupGroupSize, setupLineCount) {
|
|
3422
|
+
if (duplicateSetupGroupSize < 2 || setupLineCount < 2) {
|
|
3423
|
+
return 0;
|
|
3424
|
+
}
|
|
3425
|
+
return Math.min(4, duplicateSetupGroupSize - 1 + Math.floor((setupLineCount - 2) / 3));
|
|
3426
|
+
}
|
|
3427
|
+
function nestingPressure(describeDepth) {
|
|
3428
|
+
return Math.max(0, describeDepth - 2);
|
|
3429
|
+
}
|
|
3430
|
+
|
|
3431
|
+
// src/scrap/vitest/pressure.ts
|
|
3432
|
+
function asyncWaitPressure(asyncWaitCount) {
|
|
3433
|
+
return Math.min(4, asyncWaitCount);
|
|
3434
|
+
}
|
|
3435
|
+
function concurrencyPressure(concurrencyCount) {
|
|
3436
|
+
return Math.min(2, concurrencyCount);
|
|
3437
|
+
}
|
|
3438
|
+
function environmentMutationPressure(envMutationCount, fakeTimerCount) {
|
|
3439
|
+
return Math.min(3, envMutationCount + fakeTimerCount);
|
|
3440
|
+
}
|
|
3441
|
+
function moduleMockPressure(moduleMockCount) {
|
|
3442
|
+
return Math.min(3, moduleMockCount);
|
|
3443
|
+
}
|
|
3444
|
+
function snapshotPressure(snapshotCount) {
|
|
3445
|
+
return Math.max(0, Math.min(4, snapshotCount - 1));
|
|
3446
|
+
}
|
|
3447
|
+
function rtlQueryPressure(rtlRenderCount, rtlQueryCount, rtlMutationCount) {
|
|
3448
|
+
if (rtlRenderCount === 0 || rtlMutationCount > 0 || rtlQueryCount < 3) {
|
|
3449
|
+
return 0;
|
|
3450
|
+
}
|
|
3451
|
+
return Math.min(3, rtlQueryCount - 2);
|
|
3452
|
+
}
|
|
3453
|
+
function vitestOperationalPressure(snapshotCount, asyncWaitCount, fakeTimerCount, envMutationCount, concurrencyCount, moduleMockCount = 0, rtlRenderCount = 0, rtlQueryCount = 0, rtlMutationCount = 0) {
|
|
3454
|
+
return snapshotPressure(snapshotCount) + asyncWaitPressure(asyncWaitCount) + environmentMutationPressure(envMutationCount, fakeTimerCount) + concurrencyPressure(concurrencyCount) + moduleMockPressure(moduleMockCount) + rtlQueryPressure(rtlRenderCount, rtlQueryCount, rtlMutationCount);
|
|
3455
|
+
}
|
|
3456
|
+
|
|
3457
|
+
// src/scrap/analysis/examples/score.ts
|
|
3458
|
+
function structuralPressures(metric) {
|
|
3459
|
+
return linePressure(metric.lineCount) + branchPressure(metric.branchCount) + nestingPressure(metric.describeDepth);
|
|
3460
|
+
}
|
|
3461
|
+
function setupRelatedPressures(metric) {
|
|
3462
|
+
return duplicateSetupPressure(metric.duplicateSetupGroupSize, metric.setupLineCount) + Math.max(0, defaultZero(metric.setupDepth) - 1);
|
|
3463
|
+
}
|
|
3464
|
+
function qualityPressures(metric) {
|
|
3465
|
+
return helperHiddenPressure(metric.helperHiddenLineCount) + assertionPressure(metric.assertionCount) + mockPressure(metric.mockCount);
|
|
3466
|
+
}
|
|
3467
|
+
function defaultZero(value) {
|
|
3468
|
+
return value ?? 0;
|
|
3469
|
+
}
|
|
3470
|
+
function operationalPressures(metric) {
|
|
3471
|
+
const vitestPressure = vitestOperationalPressure(
|
|
3472
|
+
defaultZero(metric.snapshotCount),
|
|
3473
|
+
defaultZero(metric.asyncWaitCount),
|
|
3474
|
+
defaultZero(metric.fakeTimerCount),
|
|
3475
|
+
defaultZero(metric.envMutationCount),
|
|
3476
|
+
defaultZero(metric.concurrencyCount),
|
|
3477
|
+
defaultZero(metric.moduleMockCount),
|
|
3478
|
+
defaultZero(metric.rtlRenderCount),
|
|
3479
|
+
defaultZero(metric.rtlQueryCount),
|
|
3480
|
+
defaultZero(metric.rtlMutationCount)
|
|
3481
|
+
);
|
|
3482
|
+
return vitestPressure + Math.min(3, defaultZero(metric.tempResourceCount));
|
|
3483
|
+
}
|
|
3484
|
+
function scoreExample(metric) {
|
|
3485
|
+
return structuralPressures(metric) + setupRelatedPressures(metric) + qualityPressures(metric) + operationalPressures(metric);
|
|
3486
|
+
}
|
|
3487
|
+
|
|
3488
|
+
// src/scrap/calls/subjectNames.ts
|
|
3489
|
+
import * as ts22 from "typescript";
|
|
3490
|
+
var EXCLUDED_SUBJECTS = /* @__PURE__ */ new Set([
|
|
3491
|
+
"act",
|
|
3492
|
+
"afterAll",
|
|
3493
|
+
"afterEach",
|
|
3494
|
+
"beforeAll",
|
|
3495
|
+
"beforeEach",
|
|
3496
|
+
"describe",
|
|
3497
|
+
"expect",
|
|
3498
|
+
"expectTypeOf",
|
|
3499
|
+
"it",
|
|
3500
|
+
"jest",
|
|
3501
|
+
"screen",
|
|
3502
|
+
"test",
|
|
3503
|
+
"vi",
|
|
3504
|
+
"waitFor"
|
|
3505
|
+
]);
|
|
3506
|
+
function collectSubjects(nodes) {
|
|
3507
|
+
const subjects = /* @__PURE__ */ new Set();
|
|
3508
|
+
function walk2(current) {
|
|
3509
|
+
if (ts22.isCallExpression(current)) {
|
|
3510
|
+
const subject = baseCallName(current.expression);
|
|
3511
|
+
if (subject && !EXCLUDED_SUBJECTS.has(subject)) {
|
|
3512
|
+
subjects.add(subject);
|
|
3513
|
+
}
|
|
3514
|
+
}
|
|
3515
|
+
ts22.forEachChild(current, walk2);
|
|
3516
|
+
}
|
|
3517
|
+
nodes.forEach(walk2);
|
|
3518
|
+
return [...subjects].sort();
|
|
3519
|
+
}
|
|
3520
|
+
function collectSubjectNames(node) {
|
|
3521
|
+
return collectSubjects([node]);
|
|
3522
|
+
}
|
|
3523
|
+
function collectStatementSubjectNames(statements) {
|
|
3524
|
+
return collectSubjects(statements);
|
|
3525
|
+
}
|
|
3526
|
+
|
|
3527
|
+
// src/scrap/example/metrics.ts
|
|
3528
|
+
function analyzeExample(sourceFile, example, setupMetric = analyzeExampleSetup(sourceFile, example)) {
|
|
3529
|
+
const start = sourceFile.getLineAndCharacterOfPosition(example.body.getStart());
|
|
3530
|
+
const end = sourceFile.getLineAndCharacterOfPosition(example.body.getEnd());
|
|
3531
|
+
const helperUsage = analyzeHelperUsage(sourceFile, example);
|
|
3532
|
+
const rtlSignals = analyzeRtlSignals(example.body);
|
|
3533
|
+
const vitestSignals = analyzeVitestSignals(example.body);
|
|
3534
|
+
const setupNodes = setupStatements(example);
|
|
3535
|
+
const fixtureNodes = fixtureStatements(example.body);
|
|
3536
|
+
const assertionNodes = assertionStatements(example);
|
|
3537
|
+
const allNodes = allExampleStatements(example);
|
|
3538
|
+
const setupDepth = setupNodes.reduce(
|
|
3539
|
+
(maxDepth, statement) => Math.max(maxDepth, maxSetupDepth(statement)),
|
|
3540
|
+
0
|
|
3541
|
+
);
|
|
3542
|
+
const baseMetric = {
|
|
3543
|
+
assertionCount: collectCallCount(example.body, isAssertionCall),
|
|
3544
|
+
assertionFeatures: statementFeatures(assertionNodes),
|
|
3545
|
+
assertionFingerprint: statementFingerprint(assertionNodes),
|
|
3546
|
+
blockPath: example.blockPath,
|
|
3547
|
+
branchCount: countBranches(example.body),
|
|
3548
|
+
describeDepth: example.describeDepth,
|
|
3549
|
+
duplicateSetupGroupSize: 0,
|
|
3550
|
+
endLine: end.line + 1,
|
|
3551
|
+
exampleFeatures: statementFeatures(allNodes),
|
|
3552
|
+
exampleFingerprint: statementFingerprint(allNodes),
|
|
3553
|
+
fixtureFeatures: statementFeatures(fixtureNodes),
|
|
3554
|
+
fixtureFingerprint: statementFingerprint(fixtureNodes),
|
|
3555
|
+
literalShapeFingerprint: literalShapeFingerprint(allNodes),
|
|
3556
|
+
asyncWaitCount: vitestSignals.asyncWaitCount,
|
|
3557
|
+
concurrencyCount: vitestSignals.concurrencyCount,
|
|
3558
|
+
envMutationCount: vitestSignals.envMutationCount,
|
|
3559
|
+
helperCallCount: helperUsage.helperCallCount,
|
|
3560
|
+
helperHiddenLineCount: helperUsage.helperHiddenLineCount,
|
|
3561
|
+
lineCount: end.line - start.line + 1,
|
|
3562
|
+
fakeTimerCount: vitestSignals.fakeTimerCount,
|
|
3563
|
+
moduleMockCount: vitestSignals.moduleMockCount,
|
|
3564
|
+
mockCount: collectCallCount(example.body, isMockCall),
|
|
3565
|
+
name: example.name,
|
|
3566
|
+
rtlMutationCount: rtlSignals.rtlMutationCount,
|
|
3567
|
+
rtlQueryCount: rtlSignals.rtlQueryCount,
|
|
3568
|
+
rtlRenderCount: rtlSignals.rtlRenderCount,
|
|
3569
|
+
snapshotCount: vitestSignals.snapshotCount,
|
|
3570
|
+
setupDepth,
|
|
3571
|
+
setupFeatures: statementFeatures(setupNodes),
|
|
3572
|
+
setupFingerprint: setupMetric.setupFingerprint,
|
|
3573
|
+
setupLineCount: setupMetric.setupLineCount,
|
|
3574
|
+
setupSubjectNames: collectStatementSubjectNames(setupNodes),
|
|
3575
|
+
startLine: start.line + 1,
|
|
3576
|
+
subjectNames: collectSubjectNames(example.body),
|
|
3577
|
+
tableDriven: example.tableDriven,
|
|
3578
|
+
typeOnlyAssertionCount: vitestSignals.typeOnlyAssertionCount,
|
|
3579
|
+
tempResourceCount: countTempResourceWork(example.body)
|
|
3580
|
+
};
|
|
3581
|
+
return {
|
|
3582
|
+
...baseMetric,
|
|
3583
|
+
score: scoreExample(baseMetric)
|
|
3584
|
+
};
|
|
3585
|
+
}
|
|
3586
|
+
|
|
3587
|
+
// src/scrap/analysis/examples/find.ts
|
|
3588
|
+
import * as ts23 from "typescript";
|
|
3589
|
+
function exampleNode(node, describeDepth, blockPath) {
|
|
3590
|
+
const info = callInfo(node.expression);
|
|
3591
|
+
const callback = callbackArgument(node);
|
|
3592
|
+
if (!callback || info.baseName !== "it" && info.baseName !== "test") {
|
|
3593
|
+
return void 0;
|
|
3594
|
+
}
|
|
3595
|
+
return {
|
|
3596
|
+
body: callback,
|
|
3597
|
+
blockPath,
|
|
3598
|
+
describeDepth,
|
|
3599
|
+
name: literalName(node.arguments[0] ?? node),
|
|
3600
|
+
tableDriven: info.tableDriven
|
|
3601
|
+
};
|
|
3602
|
+
}
|
|
3603
|
+
function nestedDescribe(node) {
|
|
3604
|
+
const info = callInfo(node.expression);
|
|
3605
|
+
const callback = callbackArgument(node);
|
|
3606
|
+
if (!callback || info.baseName !== "describe" && info.baseName !== "context") {
|
|
3607
|
+
return void 0;
|
|
3608
|
+
}
|
|
3609
|
+
return {
|
|
3610
|
+
body: callback,
|
|
3611
|
+
name: literalName(node.arguments[0] ?? node)
|
|
3612
|
+
};
|
|
3613
|
+
}
|
|
3614
|
+
function findExamples(sourceFile) {
|
|
3615
|
+
const examples = [];
|
|
3616
|
+
function walk2(node, describeDepth, blockPath) {
|
|
3617
|
+
if (ts23.isCallExpression(node)) {
|
|
3618
|
+
const describeBlock = nestedDescribe(node);
|
|
3619
|
+
if (describeBlock) {
|
|
3620
|
+
walk2(describeBlock.body, describeDepth + 1, [...blockPath, describeBlock.name]);
|
|
3621
|
+
return;
|
|
3622
|
+
}
|
|
3623
|
+
const example = exampleNode(node, describeDepth, blockPath);
|
|
3624
|
+
if (example) {
|
|
3625
|
+
examples.push(example);
|
|
3626
|
+
}
|
|
3627
|
+
}
|
|
3628
|
+
ts23.forEachChild(node, (child) => walk2(child, describeDepth, blockPath));
|
|
3629
|
+
}
|
|
3630
|
+
walk2(sourceFile, 0, []);
|
|
3631
|
+
return examples;
|
|
3632
|
+
}
|
|
3633
|
+
|
|
3634
|
+
// src/scrap/analysis/examples/scored.ts
|
|
3635
|
+
function analyzeFileExamples(sourceFile) {
|
|
3636
|
+
const examples = findExamples(sourceFile);
|
|
3637
|
+
const setupMetrics = examples.map((example) => analyzeExampleSetup(sourceFile, example));
|
|
3638
|
+
const duplicateGroupSizes = duplicateSetupGroupSizes(setupMetrics);
|
|
3639
|
+
return examples.map((example, index) => {
|
|
3640
|
+
const metric = analyzeExample(sourceFile, example, setupMetrics[index]);
|
|
3641
|
+
const duplicateSetupGroupSize = duplicateGroupSizes[index] ?? 0;
|
|
3642
|
+
return {
|
|
3643
|
+
...metric,
|
|
3644
|
+
duplicateSetupGroupSize,
|
|
3645
|
+
score: scoreExample({
|
|
3646
|
+
...metric,
|
|
3647
|
+
duplicateSetupGroupSize
|
|
3648
|
+
})
|
|
3649
|
+
};
|
|
3650
|
+
});
|
|
3651
|
+
}
|
|
3652
|
+
|
|
3653
|
+
// src/scrap/example/countSummary.ts
|
|
3654
|
+
function countExamples(examples, predicate) {
|
|
3655
|
+
return examples.filter(predicate).length;
|
|
3656
|
+
}
|
|
3657
|
+
function summarizeExampleCounts(examples) {
|
|
3658
|
+
return {
|
|
3659
|
+
branchingExampleCount: countExamples(examples, (example) => example.branchCount > 0),
|
|
3660
|
+
duplicateSetupGroupSizes: examples.map((example) => example.duplicateSetupGroupSize),
|
|
3661
|
+
helperHiddenExampleCount: countExamples(examples, (example) => example.helperHiddenLineCount > 0),
|
|
3662
|
+
lowAssertionExampleCount: countExamples(examples, (example) => example.assertionCount <= 1),
|
|
3663
|
+
tableDrivenExampleCount: countExamples(examples, (example) => example.tableDriven === true),
|
|
3664
|
+
tempResourceExampleCount: countExamples(examples, (example) => (example.tempResourceCount ?? 0) > 0),
|
|
3665
|
+
zeroAssertionExampleCount: countExamples(examples, (example) => example.assertionCount === 0)
|
|
3666
|
+
};
|
|
3667
|
+
}
|
|
3668
|
+
|
|
3669
|
+
// src/scrap/example/scoreSummary.ts
|
|
3670
|
+
function totalScore(examples) {
|
|
3671
|
+
return examples.reduce((sum, example) => sum + example.score, 0);
|
|
3672
|
+
}
|
|
3673
|
+
function averageScore(examples) {
|
|
3674
|
+
if (examples.length === 0) {
|
|
3675
|
+
return 0;
|
|
3676
|
+
}
|
|
3677
|
+
return totalScore(examples) / examples.length;
|
|
3678
|
+
}
|
|
3679
|
+
function maxScore(examples) {
|
|
3680
|
+
return examples.reduce((max, example) => Math.max(max, example.score), 0);
|
|
3681
|
+
}
|
|
3682
|
+
function hotExampleCount(examples, threshold = 8) {
|
|
3683
|
+
return examples.filter((example) => example.score >= threshold).length;
|
|
3684
|
+
}
|
|
3685
|
+
function worstExamples(examples) {
|
|
3686
|
+
return [...examples].sort((left, right) => right.score - left.score).slice(0, 5);
|
|
3687
|
+
}
|
|
3688
|
+
function roundScore(value) {
|
|
3689
|
+
return Math.round(value * 100) / 100;
|
|
3690
|
+
}
|
|
3691
|
+
|
|
3692
|
+
// src/scrap/metrics/average/jaccard.ts
|
|
3693
|
+
function similaritySet(features) {
|
|
3694
|
+
return new Set(features ?? []);
|
|
3695
|
+
}
|
|
3696
|
+
function jaccardSimilarity(left, right) {
|
|
3697
|
+
const leftSet = similaritySet(left);
|
|
3698
|
+
const rightSet = similaritySet(right);
|
|
3699
|
+
let intersection = 0;
|
|
3700
|
+
leftSet.forEach((feature) => {
|
|
3701
|
+
if (rightSet.has(feature)) {
|
|
3702
|
+
intersection += 1;
|
|
3703
|
+
}
|
|
3704
|
+
});
|
|
3705
|
+
const union = (/* @__PURE__ */ new Set([...leftSet, ...rightSet])).size;
|
|
3706
|
+
if (union === 0) {
|
|
3707
|
+
return 0;
|
|
3708
|
+
}
|
|
3709
|
+
return intersection / union;
|
|
3710
|
+
}
|
|
3711
|
+
|
|
3712
|
+
// src/scrap/metrics/average/edges.ts
|
|
3713
|
+
function addSimilarityEdge(edges, left, right) {
|
|
3714
|
+
edges.get(left).push(right);
|
|
3715
|
+
edges.get(right).push(left);
|
|
3716
|
+
}
|
|
3717
|
+
function buildSimilarityEdges(featureLists, threshold) {
|
|
3718
|
+
const edges = /* @__PURE__ */ new Map();
|
|
3719
|
+
featureLists.forEach((features, index) => {
|
|
3720
|
+
if ((features?.length ?? 0) > 0) {
|
|
3721
|
+
edges.set(index, []);
|
|
3722
|
+
}
|
|
3723
|
+
});
|
|
3724
|
+
featureLists.forEach((leftFeatures, left) => {
|
|
3725
|
+
featureLists.slice(left + 1).forEach((rightFeatures, offset) => {
|
|
3726
|
+
const right = left + offset + 1;
|
|
3727
|
+
if (jaccardSimilarity(leftFeatures, rightFeatures) >= threshold) {
|
|
3728
|
+
addSimilarityEdge(edges, left, right);
|
|
3729
|
+
}
|
|
3730
|
+
});
|
|
3731
|
+
});
|
|
3732
|
+
return edges;
|
|
3733
|
+
}
|
|
3734
|
+
|
|
3735
|
+
// src/scrap/metrics/average/components.ts
|
|
3736
|
+
function collectComponent(start, edges, visited) {
|
|
3737
|
+
const stack = [start];
|
|
3738
|
+
const component = [];
|
|
3739
|
+
const queued = /* @__PURE__ */ new Set([start]);
|
|
3740
|
+
visited.add(start);
|
|
3741
|
+
for (let index = 0; index < stack.length; index++) {
|
|
3742
|
+
const current = stack[index];
|
|
3743
|
+
component.push(current);
|
|
3744
|
+
for (const neighbor of edges.get(current) ?? []) {
|
|
3745
|
+
if (queued.has(neighbor)) {
|
|
3746
|
+
continue;
|
|
3747
|
+
}
|
|
3748
|
+
queued.add(neighbor);
|
|
3749
|
+
visited.add(neighbor);
|
|
3750
|
+
stack.push(neighbor);
|
|
3751
|
+
}
|
|
3752
|
+
}
|
|
3753
|
+
return component;
|
|
3754
|
+
}
|
|
3755
|
+
function connectedComponents(featureLists, threshold) {
|
|
3756
|
+
const edges = buildSimilarityEdges(featureLists, threshold);
|
|
3757
|
+
const visited = /* @__PURE__ */ new Set();
|
|
3758
|
+
const components = [];
|
|
3759
|
+
for (const start of edges.keys()) {
|
|
3760
|
+
if (!visited.has(start)) {
|
|
3761
|
+
components.push(collectComponent(start, edges, visited));
|
|
3762
|
+
}
|
|
3763
|
+
}
|
|
3764
|
+
return components;
|
|
3765
|
+
}
|
|
3766
|
+
|
|
3767
|
+
// src/scrap/metrics/average/pairwise.ts
|
|
3768
|
+
function pairwiseSimilarity(featureLists) {
|
|
3769
|
+
let total = 0;
|
|
3770
|
+
let pairs = 0;
|
|
3771
|
+
featureLists.forEach((leftFeatures, left) => {
|
|
3772
|
+
featureLists.slice(left + 1).forEach((rightFeatures) => {
|
|
3773
|
+
total += jaccardSimilarity(leftFeatures, rightFeatures);
|
|
3774
|
+
pairs += 1;
|
|
3775
|
+
});
|
|
3776
|
+
});
|
|
3777
|
+
return pairs === 0 ? 0 : total / pairs;
|
|
3778
|
+
}
|
|
3779
|
+
|
|
3780
|
+
// src/scrap/metrics/average/groups.ts
|
|
3781
|
+
function featureGroupSizes(featureLists, threshold = 0.5) {
|
|
3782
|
+
const sizes = Array.from({ length: featureLists.length }, () => 0);
|
|
3783
|
+
connectedComponents(featureLists, threshold).forEach((component) => {
|
|
3784
|
+
component.forEach((index) => {
|
|
3785
|
+
sizes[index] = component.length;
|
|
3786
|
+
});
|
|
3787
|
+
});
|
|
3788
|
+
return sizes;
|
|
3789
|
+
}
|
|
3790
|
+
function shapeDiversity(featureLists, threshold = 0.5) {
|
|
3791
|
+
return connectedComponents(featureLists, threshold).length;
|
|
3792
|
+
}
|
|
3793
|
+
|
|
3794
|
+
// src/scrap/metrics/compute.ts
|
|
3795
|
+
function distinctSubjectCount(examples) {
|
|
3796
|
+
const subjects = new Set(examples.flatMap((example) => example.subjectNames ?? []));
|
|
3797
|
+
return subjects.size;
|
|
3798
|
+
}
|
|
3799
|
+
function subjectRepetitionScore(examples) {
|
|
3800
|
+
const counts = /* @__PURE__ */ new Map();
|
|
3801
|
+
examples.flatMap((example) => example.subjectNames ?? []).forEach((subject) => {
|
|
3802
|
+
counts.set(subject, (counts.get(subject) ?? 0) + 1);
|
|
3803
|
+
});
|
|
3804
|
+
return [...counts.values()].filter((count) => count > 1).length;
|
|
3805
|
+
}
|
|
3806
|
+
function analyzeCohesionMetrics(examples) {
|
|
3807
|
+
const assertionFeatures = examples.map((example) => example.assertionFeatures);
|
|
3808
|
+
const exampleFeatures = examples.map((example) => example.exampleFeatures);
|
|
3809
|
+
const fixtureFeatures = examples.map((example) => example.fixtureFeatures);
|
|
3810
|
+
const setupFeatures = examples.map((example) => example.setupFeatures);
|
|
3811
|
+
const subjectSets = examples.map((example) => example.subjectNames);
|
|
3812
|
+
return {
|
|
3813
|
+
assertionShapeDiversity: shapeDiversity(assertionFeatures),
|
|
3814
|
+
averageAssertionSimilarity: pairwiseSimilarity(assertionFeatures),
|
|
3815
|
+
averageExampleSimilarity: pairwiseSimilarity(exampleFeatures),
|
|
3816
|
+
averageFixtureSimilarity: pairwiseSimilarity(fixtureFeatures),
|
|
3817
|
+
averageSetupSimilarity: pairwiseSimilarity(setupFeatures),
|
|
3818
|
+
averageSubjectOverlap: pairwiseSimilarity(subjectSets),
|
|
3819
|
+
distinctSubjectCount: distinctSubjectCount(examples),
|
|
3820
|
+
exampleShapeDiversity: shapeDiversity(exampleFeatures),
|
|
3821
|
+
fixtureShapeDiversity: shapeDiversity(fixtureFeatures),
|
|
3822
|
+
setupShapeDiversity: shapeDiversity(setupFeatures),
|
|
3823
|
+
subjectRepetitionScore: subjectRepetitionScore(examples)
|
|
3824
|
+
};
|
|
3825
|
+
}
|
|
3826
|
+
|
|
3827
|
+
// src/scrap/metrics/cohesionPredicates.ts
|
|
3828
|
+
function hasBroadSubjectSpread(cohesion, exampleCount) {
|
|
3829
|
+
return exampleCount >= 7 && cohesion.distinctSubjectCount >= 4 && cohesion.averageSubjectOverlap <= 0.1;
|
|
3830
|
+
}
|
|
3831
|
+
function hasShapeDrift(cohesion, exampleCount) {
|
|
3832
|
+
return exampleCount >= 7 && cohesion.exampleShapeDiversity >= 3 && cohesion.averageExampleSimilarity <= 0.2 && cohesion.subjectRepetitionScore <= 1;
|
|
3833
|
+
}
|
|
3834
|
+
|
|
3835
|
+
// src/scrap/metrics/recommendations.ts
|
|
3836
|
+
function buildReasonMessage(cohesion, isBroadSpread) {
|
|
3837
|
+
if (isBroadSpread) {
|
|
3838
|
+
return `Examples touch ${cohesion.distinctSubjectCount} distinct subjects with little overlap.`;
|
|
3839
|
+
}
|
|
3840
|
+
return `Examples vary structurally (diversity ${cohesion.exampleShapeDiversity}) with low similarity (${cohesion.averageExampleSimilarity}).`;
|
|
3841
|
+
}
|
|
3842
|
+
function cohesionRecommendations(cohesion, exampleCount) {
|
|
3843
|
+
const broadSubjectSpread = hasBroadSubjectSpread(cohesion, exampleCount);
|
|
3844
|
+
const shapeDrift = hasShapeDrift(cohesion, exampleCount);
|
|
3845
|
+
if (broadSubjectSpread || shapeDrift) {
|
|
3846
|
+
const reason = buildReasonMessage(cohesion, broadSubjectSpread);
|
|
3847
|
+
return [{
|
|
3848
|
+
confidence: "LOW",
|
|
3849
|
+
kind: "REVIEW_STRUCTURE",
|
|
3850
|
+
message: `${reason} Review whether this file mixes responsibilities.`
|
|
3851
|
+
}];
|
|
3852
|
+
}
|
|
3853
|
+
return [];
|
|
3854
|
+
}
|
|
3855
|
+
|
|
3856
|
+
// src/scrap/policy/parseIssues.ts
|
|
3857
|
+
function diagnosticLine(sourceFile, diagnostic) {
|
|
3858
|
+
return sourceFile.getLineAndCharacterOfPosition(diagnostic.start ?? 0).line + 1;
|
|
3859
|
+
}
|
|
3860
|
+
function diagnosticSegments(messageText) {
|
|
3861
|
+
if (typeof messageText === "string") {
|
|
3862
|
+
return [messageText];
|
|
3863
|
+
}
|
|
3864
|
+
return [
|
|
3865
|
+
messageText.messageText,
|
|
3866
|
+
...(messageText.next ?? []).flatMap((segment) => diagnosticSegments(segment))
|
|
3867
|
+
];
|
|
3868
|
+
}
|
|
3869
|
+
function diagnosticMessage(diagnostic) {
|
|
3870
|
+
return diagnosticSegments(diagnostic.messageText).map((segment) => segment.trim()).filter((segment) => segment.length > 0).join(" ");
|
|
3871
|
+
}
|
|
3872
|
+
function parseDiagnostics(sourceFile) {
|
|
3873
|
+
return sourceFile.parseDiagnostics ?? [];
|
|
3874
|
+
}
|
|
3875
|
+
function parseIssues(sourceFile) {
|
|
3876
|
+
return parseDiagnostics(sourceFile).map((diagnostic) => ({
|
|
3877
|
+
kind: "parse",
|
|
3878
|
+
line: diagnosticLine(sourceFile, diagnostic),
|
|
3879
|
+
message: diagnosticMessage(diagnostic)
|
|
3880
|
+
}));
|
|
3881
|
+
}
|
|
3882
|
+
|
|
3883
|
+
// src/scrap/policy/structureIssues.ts
|
|
3884
|
+
import * as ts24 from "typescript";
|
|
3885
|
+
|
|
3886
|
+
// src/scrap/example/calls/callKinds.ts
|
|
3887
|
+
function isExampleCallName(callName) {
|
|
3888
|
+
return callName === "it" || callName === "test";
|
|
3889
|
+
}
|
|
3890
|
+
function nextInsideExampleState(insideExample, callName) {
|
|
3891
|
+
return insideExample || isExampleCallName(callName);
|
|
3892
|
+
}
|
|
3893
|
+
|
|
3894
|
+
// src/scrap/calls/structureCallKinds.ts
|
|
3895
|
+
function isHookCallName(callName) {
|
|
3896
|
+
return callName === "afterAll" || callName === "afterEach" || callName === "beforeAll" || callName === "beforeEach";
|
|
3897
|
+
}
|
|
3898
|
+
function isStructureCallName(callName) {
|
|
3899
|
+
return callName === "context" || callName === "describe";
|
|
3900
|
+
}
|
|
3901
|
+
function isHookOrStructureCallName(callName) {
|
|
3902
|
+
return typeof callName === "string" && (isHookCallName(callName) || isStructureCallName(callName));
|
|
3903
|
+
}
|
|
3904
|
+
|
|
3905
|
+
// src/scrap/policy/structureIssues.ts
|
|
3906
|
+
function issueLine(sourceFile, node) {
|
|
3907
|
+
return sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile)).line + 1;
|
|
3908
|
+
}
|
|
3909
|
+
function issue(sourceFile, node, kind, message) {
|
|
3910
|
+
return {
|
|
3911
|
+
kind,
|
|
3912
|
+
line: issueLine(sourceFile, node),
|
|
3913
|
+
message
|
|
3914
|
+
};
|
|
3915
|
+
}
|
|
3916
|
+
function nestedExampleIssue(sourceFile, node, callName) {
|
|
3917
|
+
return issue(
|
|
3918
|
+
sourceFile,
|
|
3919
|
+
node,
|
|
3920
|
+
"nested-test",
|
|
3921
|
+
`Nested ${callName} call inside another test body.`
|
|
3922
|
+
);
|
|
3923
|
+
}
|
|
3924
|
+
function misplacedStructureIssue(sourceFile, node, callName) {
|
|
3925
|
+
return issue(
|
|
3926
|
+
sourceFile,
|
|
3927
|
+
node,
|
|
3928
|
+
"hook-in-test",
|
|
3929
|
+
`${callName} call inside a test body should be lifted out of the example.`
|
|
3930
|
+
);
|
|
3931
|
+
}
|
|
3932
|
+
function issuesForCall(sourceFile, node, insideExample) {
|
|
3933
|
+
const callName = baseCallName(node.expression);
|
|
3934
|
+
if (!insideExample || !callName) {
|
|
3935
|
+
return [];
|
|
3936
|
+
}
|
|
3937
|
+
if (isExampleCallName(callName)) {
|
|
3938
|
+
return [nestedExampleIssue(sourceFile, node, callName)];
|
|
3939
|
+
}
|
|
3940
|
+
if (isHookOrStructureCallName(callName) && callbackArgument(node)) {
|
|
3941
|
+
return [misplacedStructureIssue(sourceFile, node, callName)];
|
|
3942
|
+
}
|
|
3943
|
+
return [];
|
|
3944
|
+
}
|
|
3945
|
+
function walk(sourceFile, node, insideExample, issues) {
|
|
3946
|
+
if (ts24.isCallExpression(node)) {
|
|
3947
|
+
issues.push(...issuesForCall(sourceFile, node, insideExample));
|
|
3948
|
+
const callback = callbackArgument(node);
|
|
3949
|
+
if (callback) {
|
|
3950
|
+
walk(
|
|
3951
|
+
sourceFile,
|
|
3952
|
+
callback,
|
|
3953
|
+
nextInsideExampleState(insideExample, baseCallName(node.expression)),
|
|
3954
|
+
issues
|
|
3955
|
+
);
|
|
3956
|
+
return;
|
|
3957
|
+
}
|
|
3958
|
+
}
|
|
3959
|
+
ts24.forEachChild(node, (child) => walk(sourceFile, child, insideExample, issues));
|
|
3960
|
+
}
|
|
3961
|
+
function structureIssues(sourceFile) {
|
|
3962
|
+
const issues = [];
|
|
3963
|
+
walk(sourceFile, sourceFile, false, issues);
|
|
3964
|
+
return issues;
|
|
3965
|
+
}
|
|
3966
|
+
|
|
3967
|
+
// src/scrap/policy/issues.ts
|
|
3968
|
+
function validateScrapFile(sourceFile) {
|
|
3969
|
+
return [...parseIssues(sourceFile), ...structureIssues(sourceFile)];
|
|
3970
|
+
}
|
|
3971
|
+
|
|
3972
|
+
// src/scrap/policy/remediationMode.ts
|
|
3973
|
+
function remediationMode(exampleCount, averageScore3, hotExampleCount2, maxScore2) {
|
|
3974
|
+
if (hotExampleCount2 >= 10 || exampleCount >= 30 && averageScore3 >= 5 || exampleCount >= 50 && averageScore3 >= 4.25) {
|
|
3975
|
+
return "SPLIT";
|
|
3976
|
+
}
|
|
3977
|
+
if (maxScore2 >= 6 || averageScore3 >= 4) {
|
|
3978
|
+
return "LOCAL";
|
|
3979
|
+
}
|
|
3980
|
+
return "STABLE";
|
|
3981
|
+
}
|
|
3982
|
+
|
|
3983
|
+
// src/scrap/structure/blocks/ordering.ts
|
|
3984
|
+
function pathLabel(summary) {
|
|
3985
|
+
return summary.path.join(" > ");
|
|
3986
|
+
}
|
|
3987
|
+
function compareBlockSummaries(left, right) {
|
|
3988
|
+
return right.maxScore - left.maxScore || right.averageScore - left.averageScore || right.exampleCount - left.exampleCount || pathLabel(left).localeCompare(pathLabel(right));
|
|
3989
|
+
}
|
|
3990
|
+
|
|
3991
|
+
// src/scrap/structure/blocks/groups.ts
|
|
3992
|
+
var BLOCK_SEPARATOR = "";
|
|
3993
|
+
function blockPathKey(path3) {
|
|
3994
|
+
return path3.join(BLOCK_SEPARATOR);
|
|
3995
|
+
}
|
|
3996
|
+
function blockPathFromKey(key) {
|
|
3997
|
+
return key.split(BLOCK_SEPARATOR);
|
|
3998
|
+
}
|
|
3999
|
+
function prefixBlockGroups(examples) {
|
|
4000
|
+
const groups = /* @__PURE__ */ new Map();
|
|
4001
|
+
examples.forEach((example) => {
|
|
4002
|
+
example.blockPath.forEach((_, depthIndex) => {
|
|
4003
|
+
const key = blockPathKey(example.blockPath.slice(0, depthIndex + 1));
|
|
4004
|
+
const group = groups.get(key) ?? [];
|
|
4005
|
+
group.push(example);
|
|
4006
|
+
groups.set(key, group);
|
|
4007
|
+
});
|
|
4008
|
+
});
|
|
4009
|
+
return groups;
|
|
4010
|
+
}
|
|
4011
|
+
|
|
4012
|
+
// src/scrap/structure/blocks/metric.ts
|
|
4013
|
+
function averageScore2(examples) {
|
|
4014
|
+
const total = examples.reduce((sum, example) => sum + example.score, 0);
|
|
4015
|
+
return total / examples.length;
|
|
4016
|
+
}
|
|
4017
|
+
function countExamples2(examples, predicate) {
|
|
4018
|
+
return examples.filter(predicate).length;
|
|
4019
|
+
}
|
|
4020
|
+
function summarizeBlock(path3, examples) {
|
|
4021
|
+
const meanScore = averageScore2(examples);
|
|
4022
|
+
const maxScore2 = examples.reduce((max, example) => Math.max(max, example.score), 0);
|
|
4023
|
+
const hotExampleCount2 = countExamples2(examples, (example) => example.score >= 8);
|
|
4024
|
+
return {
|
|
4025
|
+
averageScore: Math.round(meanScore * 100) / 100,
|
|
4026
|
+
branchingExampleCount: countExamples2(examples, (example) => example.branchCount > 0),
|
|
4027
|
+
duplicateSetupExampleCount: duplicateSetupExampleCount(
|
|
4028
|
+
examples.map((example) => example.duplicateSetupGroupSize)
|
|
4029
|
+
),
|
|
4030
|
+
exampleCount: examples.length,
|
|
4031
|
+
helperHiddenExampleCount: countExamples2(examples, (example) => example.helperHiddenLineCount > 0),
|
|
4032
|
+
hotExampleCount: hotExampleCount2,
|
|
4033
|
+
lowAssertionExampleCount: countExamples2(examples, (example) => example.assertionCount <= 1),
|
|
4034
|
+
maxScore: maxScore2,
|
|
4035
|
+
name: path3[path3.length - 1],
|
|
4036
|
+
path: path3,
|
|
4037
|
+
remediationMode: remediationMode(examples.length, meanScore, hotExampleCount2, maxScore2),
|
|
4038
|
+
zeroAssertionExampleCount: countExamples2(examples, (example) => example.assertionCount === 0)
|
|
4039
|
+
};
|
|
4040
|
+
}
|
|
4041
|
+
|
|
4042
|
+
// src/scrap/structure/blocks/summaries.ts
|
|
4043
|
+
function summarizeBlocks(examples) {
|
|
4044
|
+
return [...prefixBlockGroups(examples).entries()].map(([key, group]) => summarizeBlock(blockPathFromKey(key), group)).sort(compareBlockSummaries);
|
|
4045
|
+
}
|
|
4046
|
+
|
|
4047
|
+
// src/scrap/metrics/matrix/shape.ts
|
|
4048
|
+
function hasLowNoiseStructure(example) {
|
|
4049
|
+
return example.branchCount <= 1 && example.helperHiddenLineCount === 0 && example.mockCount === 0;
|
|
4050
|
+
}
|
|
4051
|
+
function hasCompactCoverageShape(example) {
|
|
4052
|
+
return example.lineCount <= 12 && (example.setupLineCount ?? 0) <= 3 && (example.tempResourceCount ?? 0) <= 1 && example.assertionCount >= 1;
|
|
4053
|
+
}
|
|
4054
|
+
function isSimpleCoverageMatrixShape(example) {
|
|
4055
|
+
return hasLowNoiseStructure(example) && hasCompactCoverageShape(example);
|
|
4056
|
+
}
|
|
4057
|
+
|
|
4058
|
+
// src/scrap/metrics/matrix/variation.ts
|
|
4059
|
+
function hasStructuredVariation(example, literalShapeGroupSize = 0, fixtureGroupSize = 0) {
|
|
4060
|
+
return example.tableDriven === true || literalShapeGroupSize > 1 || fixtureGroupSize > 1;
|
|
4061
|
+
}
|
|
4062
|
+
|
|
4063
|
+
// src/scrap/metrics/matrix/candidates.ts
|
|
4064
|
+
function isCoverageMatrixCandidate(example, duplicateSize, literalShapeGroupSize = 0, fixtureGroupSize = 0) {
|
|
4065
|
+
if (duplicateSize <= 1) {
|
|
4066
|
+
return false;
|
|
4067
|
+
}
|
|
4068
|
+
if (example.tableDriven === true) {
|
|
4069
|
+
return true;
|
|
4070
|
+
}
|
|
4071
|
+
const structuredVariation = hasStructuredVariation(
|
|
4072
|
+
example,
|
|
4073
|
+
literalShapeGroupSize,
|
|
4074
|
+
fixtureGroupSize
|
|
4075
|
+
);
|
|
4076
|
+
return structuredVariation && isSimpleCoverageMatrixShape(example);
|
|
4077
|
+
}
|
|
4078
|
+
function coverageMatrixCandidateCount(examples, groupSizes) {
|
|
4079
|
+
return examples.filter((example, index) => isCoverageMatrixCandidate(
|
|
4080
|
+
example,
|
|
4081
|
+
groupSizes.exampleGroupSizes[index] ?? 0,
|
|
4082
|
+
groupSizes.literalShapeGroupSizes[index] ?? 0,
|
|
4083
|
+
groupSizes.fixtureGroupSizes[index] ?? 0
|
|
4084
|
+
)).length;
|
|
4085
|
+
}
|
|
4086
|
+
function tableDriveCandidateCount(examples, groupSizes) {
|
|
4087
|
+
return examples.filter((example, index) => example.tableDriven !== true && isCoverageMatrixCandidate(
|
|
4088
|
+
example,
|
|
4089
|
+
groupSizes.exampleGroupSizes[index] ?? 0,
|
|
4090
|
+
groupSizes.literalShapeGroupSizes[index] ?? 0,
|
|
4091
|
+
groupSizes.fixtureGroupSizes[index] ?? 0
|
|
4092
|
+
)).length;
|
|
4093
|
+
}
|
|
4094
|
+
|
|
4095
|
+
// src/scrap/test/duplication/groupSizes.ts
|
|
4096
|
+
function selectedFingerprint(example, selector) {
|
|
4097
|
+
return selector(example);
|
|
4098
|
+
}
|
|
4099
|
+
function countedFingerprintGroups(examples, selector) {
|
|
4100
|
+
const counts = /* @__PURE__ */ new Map();
|
|
4101
|
+
examples.forEach((example) => {
|
|
4102
|
+
const fingerprint = selectedFingerprint(example, selector);
|
|
4103
|
+
if (fingerprint) {
|
|
4104
|
+
counts.set(fingerprint, (counts.get(fingerprint) ?? 0) + 1);
|
|
4105
|
+
}
|
|
4106
|
+
});
|
|
4107
|
+
return examples.map((example) => {
|
|
4108
|
+
const fingerprint = selectedFingerprint(example, selector);
|
|
4109
|
+
return fingerprint ? counts.get(fingerprint) ?? 0 : 0;
|
|
4110
|
+
});
|
|
4111
|
+
}
|
|
4112
|
+
function duplicateGroupCount(groupSizes) {
|
|
4113
|
+
return groupSizes.filter((groupSize) => groupSize > 1).length;
|
|
4114
|
+
}
|
|
4115
|
+
|
|
4116
|
+
// src/scrap/example/clusters.ts
|
|
4117
|
+
function isRepeatedSetupExample(example) {
|
|
4118
|
+
return example.duplicateSetupGroupSize > 1 && example.setupLineCount >= 2 && typeof example.setupFingerprint === "string";
|
|
4119
|
+
}
|
|
4120
|
+
function repeatedSetupExamples(examples) {
|
|
4121
|
+
return examples.filter(isRepeatedSetupExample);
|
|
4122
|
+
}
|
|
4123
|
+
function groupSetupExamples(examples) {
|
|
4124
|
+
const clusters = /* @__PURE__ */ new Map();
|
|
4125
|
+
repeatedSetupExamples(examples).forEach((example) => {
|
|
4126
|
+
const fingerprint = example.setupFingerprint;
|
|
4127
|
+
const cluster = clusters.get(fingerprint) ?? [];
|
|
4128
|
+
cluster.push(example);
|
|
4129
|
+
clusters.set(fingerprint, cluster);
|
|
4130
|
+
});
|
|
4131
|
+
return [...clusters.values()];
|
|
4132
|
+
}
|
|
4133
|
+
function strongestSetupCluster(examples) {
|
|
4134
|
+
return groupSetupExamples(examples).sort((left, right) => right.length - left.length)[0] ?? [];
|
|
4135
|
+
}
|
|
4136
|
+
function coverageRelevantExamples(examples) {
|
|
4137
|
+
return examples.filter(
|
|
4138
|
+
(example) => example.tableDriven === true || !!example.literalShapeFingerprint || !!example.fixtureFingerprint
|
|
4139
|
+
);
|
|
4140
|
+
}
|
|
4141
|
+
|
|
4142
|
+
// src/scrap/report/blocks/recommendationText.ts
|
|
4143
|
+
function uniqueNonEmptyValues(values) {
|
|
4144
|
+
return [...new Set(values)].filter((value) => value.length > 0);
|
|
4145
|
+
}
|
|
4146
|
+
function summarizeValues(label, values) {
|
|
4147
|
+
const summarized = uniqueNonEmptyValues(values).slice(0, 3);
|
|
4148
|
+
return summarized.length === 0 ? "" : ` ${label}: ${summarized.join(", ")}.`;
|
|
4149
|
+
}
|
|
4150
|
+
function summarizeBlockPaths(examples) {
|
|
4151
|
+
return summarizeValues(
|
|
4152
|
+
"Affected blocks",
|
|
4153
|
+
examples.map((example) => example.blockPath.join(" > "))
|
|
4154
|
+
);
|
|
4155
|
+
}
|
|
4156
|
+
function summarizeHelperGroups(examples) {
|
|
4157
|
+
return summarizeValues(
|
|
4158
|
+
"Helper groups",
|
|
4159
|
+
examples.flatMap((example) => example.setupSubjectNames ?? [])
|
|
4160
|
+
);
|
|
4161
|
+
}
|
|
4162
|
+
|
|
4163
|
+
// src/scrap/test/duplication/recommendations.ts
|
|
4164
|
+
function strengthenAssertionsRecommendation(zeroAssertionCount) {
|
|
4165
|
+
if (zeroAssertionCount === 0) {
|
|
4166
|
+
return [];
|
|
4167
|
+
}
|
|
4168
|
+
return [{
|
|
4169
|
+
confidence: "HIGH",
|
|
4170
|
+
kind: "STRENGTHEN_ASSERTIONS",
|
|
4171
|
+
message: `${zeroAssertionCount} example(s) have no assertions and should be tightened before structural cleanup.`
|
|
4172
|
+
}];
|
|
4173
|
+
}
|
|
4174
|
+
function tableDriveRecommendation(examples, tableDriveCandidateCount2) {
|
|
4175
|
+
if (tableDriveCandidateCount2 === 0) {
|
|
4176
|
+
return [];
|
|
4177
|
+
}
|
|
4178
|
+
return [{
|
|
4179
|
+
confidence: "HIGH",
|
|
4180
|
+
kind: "TABLE_DRIVE",
|
|
4181
|
+
message: `${tableDriveCandidateCount2} example(s) look like a coverage matrix that should be table-driven.${summarizeBlockPaths(coverageRelevantExamples(examples))}`
|
|
4182
|
+
}];
|
|
4183
|
+
}
|
|
4184
|
+
function extractSetupRecommendation(examples, repeatedSetupCount) {
|
|
4185
|
+
if (repeatedSetupCount === 0) {
|
|
4186
|
+
return [];
|
|
4187
|
+
}
|
|
4188
|
+
const strongestCluster = strongestSetupCluster(examples);
|
|
4189
|
+
return [{
|
|
4190
|
+
confidence: "MEDIUM",
|
|
4191
|
+
kind: "EXTRACT_SETUP",
|
|
4192
|
+
message: `${repeatedSetupCount} repeated setup cluster(s) look worth extracting into shared helpers or fixtures.${summarizeBlockPaths(strongestCluster)}${summarizeHelperGroups(strongestCluster)}`
|
|
4193
|
+
}];
|
|
4194
|
+
}
|
|
4195
|
+
function duplicationRecommendations(examples, counts) {
|
|
4196
|
+
return [
|
|
4197
|
+
...strengthenAssertionsRecommendation(counts.zeroAssertionCount),
|
|
4198
|
+
...tableDriveRecommendation(examples, counts.tableDriveCandidateCount),
|
|
4199
|
+
...extractSetupRecommendation(examples, counts.recommendedExtractionCount)
|
|
4200
|
+
];
|
|
4201
|
+
}
|
|
4202
|
+
|
|
4203
|
+
// src/scrap/report/blocks/extractionCount.ts
|
|
4204
|
+
function isExtractableSetup(example) {
|
|
4205
|
+
return example.duplicateSetupGroupSize > 1 && example.setupLineCount >= 2 && typeof example.setupFingerprint === "string";
|
|
4206
|
+
}
|
|
4207
|
+
function recommendedExtractionCount(examples) {
|
|
4208
|
+
return new Set(
|
|
4209
|
+
examples.filter(isExtractableSetup).map((example) => example.setupFingerprint)
|
|
4210
|
+
).size;
|
|
4211
|
+
}
|
|
4212
|
+
|
|
4213
|
+
// src/scrap/test/duplication/insights.ts
|
|
4214
|
+
function fuzzyGroups(examples, selector) {
|
|
4215
|
+
return featureGroupSizes(examples.map(selector));
|
|
4216
|
+
}
|
|
4217
|
+
function resolvedGroups(examples, fuzzyGroupSizes, selector) {
|
|
4218
|
+
return fuzzyGroupSizes.some((groupSize) => groupSize > 0) ? fuzzyGroupSizes : countedFingerprintGroups(examples, selector);
|
|
4219
|
+
}
|
|
4220
|
+
function setupGroupSizes(examples) {
|
|
4221
|
+
const fuzzySetupGroups = fuzzyGroups(examples, (example) => example.setupFeatures);
|
|
4222
|
+
return examples.map(
|
|
4223
|
+
(example, index) => Math.max(fuzzySetupGroups[index] ?? 0, example.duplicateSetupGroupSize)
|
|
4224
|
+
);
|
|
4225
|
+
}
|
|
4226
|
+
function analyzeDuplicationInsights(examples) {
|
|
4227
|
+
const resolvedSetupGroupSizes = setupGroupSizes(examples);
|
|
4228
|
+
const assertionGroupSizes = resolvedGroups(
|
|
4229
|
+
examples,
|
|
4230
|
+
fuzzyGroups(examples, (example) => example.assertionFeatures),
|
|
4231
|
+
(example) => example.assertionFingerprint
|
|
4232
|
+
);
|
|
4233
|
+
const fixtureGroupSizes = resolvedGroups(
|
|
4234
|
+
examples,
|
|
4235
|
+
fuzzyGroups(examples, (example) => example.fixtureFeatures),
|
|
4236
|
+
(example) => example.fixtureFingerprint
|
|
4237
|
+
);
|
|
4238
|
+
const literalShapeGroupSizes = countedFingerprintGroups(
|
|
4239
|
+
examples,
|
|
4240
|
+
(example) => example.literalShapeFingerprint
|
|
4241
|
+
);
|
|
4242
|
+
const exampleGroupSizes = resolvedGroups(
|
|
4243
|
+
examples,
|
|
4244
|
+
fuzzyGroups(examples, (example) => example.exampleFeatures),
|
|
4245
|
+
(example) => example.exampleFingerprint
|
|
4246
|
+
);
|
|
4247
|
+
const zeroAssertionCount = examples.filter((example) => example.assertionCount === 0).length;
|
|
4248
|
+
const setupDuplicationScore = duplicateGroupCount(resolvedSetupGroupSizes);
|
|
4249
|
+
const assertionDuplicationScore = duplicateGroupCount(assertionGroupSizes);
|
|
4250
|
+
const fixtureDuplicationScore = duplicateGroupCount(fixtureGroupSizes);
|
|
4251
|
+
const literalDuplicationScore = duplicateGroupCount(literalShapeGroupSizes);
|
|
4252
|
+
const coverageMatrixCandidates = coverageMatrixCandidateCount(examples, {
|
|
4253
|
+
exampleGroupSizes,
|
|
4254
|
+
fixtureGroupSizes,
|
|
4255
|
+
literalShapeGroupSizes
|
|
4256
|
+
});
|
|
4257
|
+
const tableDriveCandidates = tableDriveCandidateCount(examples, {
|
|
4258
|
+
exampleGroupSizes,
|
|
4259
|
+
fixtureGroupSizes,
|
|
4260
|
+
literalShapeGroupSizes
|
|
4261
|
+
});
|
|
4262
|
+
const harmfulDuplicationScore = setupDuplicationScore + assertionDuplicationScore + fixtureDuplicationScore;
|
|
4263
|
+
const effectiveDuplicationScore = Math.max(0, harmfulDuplicationScore - coverageMatrixCandidates);
|
|
4264
|
+
const extractionPressureScore = Math.max(
|
|
4265
|
+
0,
|
|
4266
|
+
setupDuplicationScore + fixtureDuplicationScore - coverageMatrixCandidates
|
|
4267
|
+
);
|
|
4268
|
+
const repeatedSetupCount = recommendedExtractionCount(examples);
|
|
4269
|
+
const recommendations = duplicationRecommendations(examples, {
|
|
4270
|
+
coverageMatrixCandidateCount: coverageMatrixCandidates,
|
|
4271
|
+
recommendedExtractionCount: repeatedSetupCount,
|
|
4272
|
+
tableDriveCandidateCount: tableDriveCandidates,
|
|
4273
|
+
zeroAssertionCount
|
|
4274
|
+
});
|
|
4275
|
+
return {
|
|
4276
|
+
assertionDuplicationScore,
|
|
4277
|
+
coverageMatrixCandidateCount: coverageMatrixCandidates,
|
|
4278
|
+
effectiveDuplicationScore,
|
|
4279
|
+
extractionPressureScore,
|
|
4280
|
+
harmfulDuplicationScore,
|
|
4281
|
+
fixtureDuplicationScore,
|
|
4282
|
+
literalDuplicationScore,
|
|
4283
|
+
recommendations,
|
|
4284
|
+
recommendedExtractionCount: repeatedSetupCount,
|
|
4285
|
+
setupDuplicationScore
|
|
4286
|
+
};
|
|
4287
|
+
}
|
|
4288
|
+
|
|
4289
|
+
// src/scrap/vitest/predicates.ts
|
|
4290
|
+
function countExamples3(examples, predicate) {
|
|
4291
|
+
return examples.filter(predicate).length;
|
|
4292
|
+
}
|
|
4293
|
+
function hasAsyncWait(ex) {
|
|
4294
|
+
return (ex.asyncWaitCount ?? 0) > 0;
|
|
4295
|
+
}
|
|
4296
|
+
function hasConcurrency(ex) {
|
|
4297
|
+
return (ex.concurrencyCount ?? 0) > 0;
|
|
4298
|
+
}
|
|
4299
|
+
function hasEnvMutation(ex) {
|
|
4300
|
+
return (ex.envMutationCount ?? 0) > 0;
|
|
4301
|
+
}
|
|
4302
|
+
function hasFakeTimer(ex) {
|
|
4303
|
+
return (ex.fakeTimerCount ?? 0) > 0;
|
|
4304
|
+
}
|
|
4305
|
+
function hasModuleMock(ex) {
|
|
4306
|
+
return (ex.moduleMockCount ?? 0) > 0;
|
|
4307
|
+
}
|
|
4308
|
+
|
|
4309
|
+
// src/scrap/vitest/rtlPredicates.ts
|
|
4310
|
+
function hasRtlMutation(ex) {
|
|
4311
|
+
return (ex.rtlMutationCount ?? 0) > 0;
|
|
4312
|
+
}
|
|
4313
|
+
function isRtlQueryHeavy(ex) {
|
|
4314
|
+
return (ex.rtlRenderCount ?? 0) > 0 && (ex.rtlQueryCount ?? 0) >= 3 && (ex.rtlMutationCount ?? 0) === 0;
|
|
4315
|
+
}
|
|
4316
|
+
function hasRtlRender(ex) {
|
|
4317
|
+
return (ex.rtlRenderCount ?? 0) > 0;
|
|
4318
|
+
}
|
|
4319
|
+
function hasSnapshot(ex) {
|
|
4320
|
+
return (ex.snapshotCount ?? 0) > 0;
|
|
4321
|
+
}
|
|
4322
|
+
function hasTypeOnlyAssertion(ex) {
|
|
4323
|
+
return (ex.typeOnlyAssertionCount ?? 0) > 0;
|
|
4324
|
+
}
|
|
4325
|
+
|
|
4326
|
+
// src/scrap/vitest/signalSummary.ts
|
|
4327
|
+
function summarizeVitestSignals(examples) {
|
|
4328
|
+
return {
|
|
4329
|
+
asyncWaitExampleCount: countExamples3(examples, hasAsyncWait),
|
|
4330
|
+
concurrencyExampleCount: countExamples3(examples, hasConcurrency),
|
|
4331
|
+
envMutationExampleCount: countExamples3(examples, hasEnvMutation),
|
|
4332
|
+
fakeTimerExampleCount: countExamples3(examples, hasFakeTimer),
|
|
4333
|
+
moduleMockExampleCount: countExamples3(examples, hasModuleMock),
|
|
4334
|
+
rtlMutationExampleCount: countExamples3(examples, hasRtlMutation),
|
|
4335
|
+
rtlQueryHeavyExampleCount: countExamples3(examples, isRtlQueryHeavy),
|
|
4336
|
+
rtlRenderExampleCount: countExamples3(examples, hasRtlRender),
|
|
4337
|
+
snapshotExampleCount: countExamples3(examples, hasSnapshot),
|
|
4338
|
+
typeOnlyAssertionExampleCount: countExamples3(examples, hasTypeOnlyAssertion)
|
|
4339
|
+
};
|
|
4340
|
+
}
|
|
4341
|
+
|
|
4342
|
+
// src/scrap/analysis/pipeline/metrics.ts
|
|
4343
|
+
function analyzeScrapFile(sourceFile) {
|
|
4344
|
+
const examples = analyzeFileExamples(sourceFile);
|
|
4345
|
+
const validationIssues = validateScrapFile(sourceFile);
|
|
4346
|
+
const duplicationInsights = analyzeDuplicationInsights(examples);
|
|
4347
|
+
const cohesion = analyzeCohesionMetrics(examples);
|
|
4348
|
+
const blockSummaries = summarizeBlocks(examples);
|
|
4349
|
+
const counts = summarizeExampleCounts(examples);
|
|
4350
|
+
const vitestSignals = summarizeVitestSignals(examples);
|
|
4351
|
+
const exampleCount = examples.length;
|
|
4352
|
+
const meanScore = averageScore(examples);
|
|
4353
|
+
const maxExampleScore = maxScore(examples);
|
|
4354
|
+
const hotExamples = hotExampleCount(examples);
|
|
4355
|
+
const metric = {
|
|
4356
|
+
averageScore: roundScore(meanScore),
|
|
4357
|
+
averageAssertionSimilarity: roundScore(cohesion.averageAssertionSimilarity),
|
|
4358
|
+
averageExampleSimilarity: roundScore(cohesion.averageExampleSimilarity),
|
|
4359
|
+
averageFixtureSimilarity: roundScore(cohesion.averageFixtureSimilarity),
|
|
4360
|
+
averageSetupSimilarity: roundScore(cohesion.averageSetupSimilarity),
|
|
4361
|
+
averageSubjectOverlap: roundScore(cohesion.averageSubjectOverlap),
|
|
4362
|
+
assertionShapeDiversity: cohesion.assertionShapeDiversity,
|
|
4363
|
+
asyncWaitExampleCount: vitestSignals.asyncWaitExampleCount,
|
|
4364
|
+
branchingExampleCount: counts.branchingExampleCount,
|
|
4365
|
+
blockSummaries,
|
|
4366
|
+
coverageMatrixCandidateCount: duplicationInsights.coverageMatrixCandidateCount,
|
|
4367
|
+
concurrencyExampleCount: vitestSignals.concurrencyExampleCount,
|
|
4368
|
+
distinctSubjectCount: cohesion.distinctSubjectCount,
|
|
4369
|
+
duplicateSetupExampleCount: duplicateSetupExampleCount(counts.duplicateSetupGroupSizes),
|
|
4370
|
+
effectiveDuplicationScore: duplicationInsights.effectiveDuplicationScore,
|
|
4371
|
+
exampleCount,
|
|
4372
|
+
exampleShapeDiversity: cohesion.exampleShapeDiversity,
|
|
4373
|
+
extractionPressureScore: duplicationInsights.extractionPressureScore,
|
|
4374
|
+
filePath: sourceFile.fileName,
|
|
4375
|
+
fakeTimerExampleCount: vitestSignals.fakeTimerExampleCount,
|
|
4376
|
+
moduleMockExampleCount: vitestSignals.moduleMockExampleCount,
|
|
4377
|
+
harmfulDuplicationScore: duplicationInsights.harmfulDuplicationScore,
|
|
4378
|
+
fixtureDuplicationScore: duplicationInsights.fixtureDuplicationScore,
|
|
4379
|
+
helperHiddenExampleCount: counts.helperHiddenExampleCount,
|
|
4380
|
+
literalDuplicationScore: duplicationInsights.literalDuplicationScore,
|
|
4381
|
+
envMutationExampleCount: vitestSignals.envMutationExampleCount,
|
|
4382
|
+
lowAssertionExampleCount: counts.lowAssertionExampleCount,
|
|
4383
|
+
maxScore: maxExampleScore,
|
|
4384
|
+
recommendations: [
|
|
4385
|
+
...duplicationInsights.recommendations,
|
|
4386
|
+
...cohesionRecommendations(cohesion, exampleCount)
|
|
4387
|
+
],
|
|
4388
|
+
recommendedExtractionCount: duplicationInsights.recommendedExtractionCount,
|
|
4389
|
+
remediationMode: remediationMode(exampleCount, meanScore, hotExamples, maxExampleScore),
|
|
4390
|
+
rtlMutationExampleCount: vitestSignals.rtlMutationExampleCount,
|
|
4391
|
+
rtlQueryHeavyExampleCount: vitestSignals.rtlQueryHeavyExampleCount,
|
|
4392
|
+
rtlRenderExampleCount: vitestSignals.rtlRenderExampleCount,
|
|
4393
|
+
snapshotExampleCount: vitestSignals.snapshotExampleCount,
|
|
4394
|
+
setupDuplicationScore: duplicationInsights.setupDuplicationScore,
|
|
4395
|
+
fixtureShapeDiversity: cohesion.fixtureShapeDiversity,
|
|
4396
|
+
setupShapeDiversity: cohesion.setupShapeDiversity,
|
|
4397
|
+
subjectRepetitionScore: cohesion.subjectRepetitionScore,
|
|
4398
|
+
tableDrivenExampleCount: counts.tableDrivenExampleCount,
|
|
4399
|
+
typeOnlyAssertionExampleCount: vitestSignals.typeOnlyAssertionExampleCount,
|
|
4400
|
+
tempResourceExampleCount: counts.tempResourceExampleCount,
|
|
4401
|
+
validationIssues,
|
|
4402
|
+
worstExamples: worstExamples(examples),
|
|
4403
|
+
zeroAssertionExampleCount: counts.zeroAssertionExampleCount
|
|
4404
|
+
};
|
|
4405
|
+
return {
|
|
4406
|
+
...metric,
|
|
4407
|
+
aiActionability: aiActionability(metric)
|
|
4408
|
+
};
|
|
4409
|
+
}
|
|
4410
|
+
|
|
4411
|
+
// src/scrap/analysis/pipeline/run.ts
|
|
4412
|
+
function analyzeScrap(target) {
|
|
4413
|
+
return discoverTestFiles(target).map((filePath) => {
|
|
4414
|
+
const source = fs.readFileSync(filePath, "utf-8");
|
|
4415
|
+
const sourceFile = ts25.createSourceFile(
|
|
4416
|
+
filePath,
|
|
4417
|
+
source,
|
|
4418
|
+
ts25.ScriptTarget.Latest,
|
|
4419
|
+
true,
|
|
4420
|
+
filePath.endsWith(".tsx") ? ts25.ScriptKind.TSX : ts25.ScriptKind.TS
|
|
4421
|
+
);
|
|
4422
|
+
return analyzeScrapFile(sourceFile);
|
|
4423
|
+
});
|
|
4424
|
+
}
|
|
4425
|
+
|
|
4426
|
+
// src/scrap/report/verdict.ts
|
|
4427
|
+
function includesNegative(values) {
|
|
4428
|
+
return values.some((value) => value < 0);
|
|
4429
|
+
}
|
|
4430
|
+
function includesPositive(values) {
|
|
4431
|
+
return values.some((value) => value > 0);
|
|
4432
|
+
}
|
|
4433
|
+
function verdictFromDeltas2(comparison) {
|
|
4434
|
+
const deltas = [
|
|
4435
|
+
comparison.averageScoreDelta,
|
|
4436
|
+
comparison.maxScoreDelta,
|
|
4437
|
+
comparison.extractionPressureDelta,
|
|
4438
|
+
comparison.harmfulDuplicationDelta,
|
|
4439
|
+
comparison.coverageMatrixDelta,
|
|
4440
|
+
comparison.helperHiddenDelta
|
|
4441
|
+
];
|
|
4442
|
+
const hasImprovement = includesNegative(deltas);
|
|
4443
|
+
const hasRegression = includesPositive(deltas);
|
|
4444
|
+
if (hasImprovement && hasRegression) {
|
|
4445
|
+
return "mixed";
|
|
4446
|
+
}
|
|
4447
|
+
if (hasRegression) {
|
|
4448
|
+
return "worse";
|
|
4449
|
+
}
|
|
4450
|
+
if (hasImprovement) {
|
|
4451
|
+
return "improved";
|
|
4452
|
+
}
|
|
4453
|
+
return "unchanged";
|
|
4454
|
+
}
|
|
4455
|
+
|
|
4456
|
+
// src/scrap/test/metric.ts
|
|
4457
|
+
function roundedDelta2(current, previous) {
|
|
4458
|
+
return Math.round((current - previous) * 100) / 100;
|
|
4459
|
+
}
|
|
4460
|
+
function metricNumbers(metric) {
|
|
4461
|
+
return {
|
|
4462
|
+
averageScore: metric.averageScore,
|
|
4463
|
+
coverageMatrixCandidateCount: metric.coverageMatrixCandidateCount ?? 0,
|
|
4464
|
+
extractionPressureScore: metric.extractionPressureScore ?? 0,
|
|
4465
|
+
harmfulDuplicationScore: metric.harmfulDuplicationScore ?? 0,
|
|
4466
|
+
helperHiddenExampleCount: metric.helperHiddenExampleCount,
|
|
4467
|
+
maxScore: metric.maxScore
|
|
4468
|
+
};
|
|
4469
|
+
}
|
|
4470
|
+
function baselineNumbers(metric) {
|
|
4471
|
+
return {
|
|
4472
|
+
averageScore: metric.averageScore ?? 0,
|
|
4473
|
+
coverageMatrixCandidateCount: metric.coverageMatrixCandidateCount ?? 0,
|
|
4474
|
+
extractionPressureScore: metric.extractionPressureScore ?? 0,
|
|
4475
|
+
harmfulDuplicationScore: metric.harmfulDuplicationScore ?? 0,
|
|
4476
|
+
helperHiddenExampleCount: metric.helperHiddenExampleCount ?? 0,
|
|
4477
|
+
maxScore: metric.maxScore ?? 0
|
|
4478
|
+
};
|
|
4479
|
+
}
|
|
4480
|
+
function deltaSnapshot(current, previous) {
|
|
4481
|
+
return {
|
|
4482
|
+
averageScoreDelta: roundedDelta2(current.averageScore, previous.averageScore),
|
|
4483
|
+
coverageMatrixDelta: roundedDelta2(
|
|
4484
|
+
current.coverageMatrixCandidateCount,
|
|
4485
|
+
previous.coverageMatrixCandidateCount
|
|
4486
|
+
),
|
|
4487
|
+
extractionPressureDelta: roundedDelta2(
|
|
4488
|
+
current.extractionPressureScore,
|
|
4489
|
+
previous.extractionPressureScore
|
|
4490
|
+
),
|
|
4491
|
+
harmfulDuplicationDelta: roundedDelta2(
|
|
4492
|
+
current.harmfulDuplicationScore,
|
|
4493
|
+
previous.harmfulDuplicationScore
|
|
4494
|
+
),
|
|
4495
|
+
helperHiddenDelta: roundedDelta2(
|
|
4496
|
+
current.helperHiddenExampleCount,
|
|
4497
|
+
previous.helperHiddenExampleCount
|
|
4498
|
+
),
|
|
4499
|
+
maxScoreDelta: roundedDelta2(current.maxScore, previous.maxScore)
|
|
4500
|
+
};
|
|
4501
|
+
}
|
|
4502
|
+
function comparisonForMetric(current, previous) {
|
|
4503
|
+
if (!previous) {
|
|
4504
|
+
return void 0;
|
|
4505
|
+
}
|
|
4506
|
+
const comparison = deltaSnapshot(metricNumbers(current), baselineNumbers(previous));
|
|
4507
|
+
return {
|
|
4508
|
+
...comparison,
|
|
4509
|
+
verdict: verdictFromDeltas2(comparison)
|
|
4510
|
+
};
|
|
4511
|
+
}
|
|
4512
|
+
|
|
4513
|
+
// src/scrap/test/compare.ts
|
|
4514
|
+
function applyBaselineComparison(metrics, baselinePath) {
|
|
4515
|
+
const previousByPath = baselineMetricsByPath2(readBaselineMetrics(baselinePath));
|
|
4516
|
+
return metrics.map((metric) => ({
|
|
4517
|
+
...metric,
|
|
4518
|
+
comparison: comparisonForMetric(metric, previousByPath.get(metric.filePath))
|
|
4519
|
+
}));
|
|
4520
|
+
}
|
|
4521
|
+
|
|
4522
|
+
// src/scrap/policy/violations.ts
|
|
4523
|
+
function hasSplitViolation(metric) {
|
|
4524
|
+
return metric.remediationMode === "SPLIT";
|
|
4525
|
+
}
|
|
4526
|
+
function hasReviewFirstViolation(metric) {
|
|
4527
|
+
return metric.aiActionability === "REVIEW_FIRST";
|
|
4528
|
+
}
|
|
4529
|
+
function hasPolicyViolations(metrics, policy) {
|
|
4530
|
+
switch (policy) {
|
|
4531
|
+
case "split":
|
|
4532
|
+
return metrics.some(hasSplitViolation);
|
|
4533
|
+
case "review":
|
|
4534
|
+
return metrics.some(hasReviewFirstViolation);
|
|
4535
|
+
case "strict":
|
|
4536
|
+
return metrics.some(hasSplitViolation) || metrics.some(hasReviewFirstViolation);
|
|
4537
|
+
case "advisory":
|
|
4538
|
+
default:
|
|
4539
|
+
return false;
|
|
4540
|
+
}
|
|
4541
|
+
}
|
|
4542
|
+
|
|
4543
|
+
// src/scrap/policy/failureMessage.ts
|
|
4544
|
+
function policyFailureMessage(policy) {
|
|
4545
|
+
if (policy === "split") {
|
|
4546
|
+
return "SCRAP split policy failed: split files are present.";
|
|
4547
|
+
}
|
|
4548
|
+
if (policy === "review") {
|
|
4549
|
+
return "SCRAP review policy failed: review-first files are present.";
|
|
4550
|
+
}
|
|
4551
|
+
if (policy === "strict") {
|
|
4552
|
+
return "SCRAP strict mode failed: split or review-first files are present.";
|
|
4553
|
+
}
|
|
4554
|
+
return void 0;
|
|
4555
|
+
}
|
|
4556
|
+
|
|
4557
|
+
// src/scrap/policy/resolve.ts
|
|
4558
|
+
function resolveScrapPolicy(args2) {
|
|
4559
|
+
const preset = flagValue(args2, "--policy");
|
|
4560
|
+
if (preset === "strict" || preset === "advisory" || preset === "review" || preset === "split") {
|
|
4561
|
+
return preset;
|
|
4562
|
+
}
|
|
4563
|
+
if (preset !== void 0) {
|
|
4564
|
+
throw new Error(`Unknown SCRAP policy preset: ${preset}`);
|
|
4565
|
+
}
|
|
4566
|
+
return args2.includes("--strict") ? "strict" : "advisory";
|
|
4567
|
+
}
|
|
4568
|
+
|
|
4569
|
+
// src/scrap/report/blocks/format.ts
|
|
4570
|
+
function formatBlockPath(path3) {
|
|
4571
|
+
return path3.join(" > ");
|
|
4572
|
+
}
|
|
4573
|
+
function interestingBlocks(metric) {
|
|
4574
|
+
return metric.blockSummaries.filter((block) => block.remediationMode !== "STABLE").slice(0, 5);
|
|
4575
|
+
}
|
|
4576
|
+
function hotBlockLines(metric) {
|
|
4577
|
+
const hotBlocks = interestingBlocks(metric);
|
|
4578
|
+
if (hotBlocks.length === 0) {
|
|
4579
|
+
return [];
|
|
4580
|
+
}
|
|
4581
|
+
return [
|
|
4582
|
+
" hot blocks:",
|
|
4583
|
+
...hotBlocks.map(
|
|
4584
|
+
(block) => ` - ${formatBlockPath(block.path)} mode=${block.remediationMode} examples=${block.exampleCount} avg/max=${block.averageScore} / ${block.maxScore} hot=${block.hotExampleCount} dupes=${block.duplicateSetupExampleCount} helpers=${block.helperHiddenExampleCount} extract=${block.recommendedExtractionCount ?? 0}`
|
|
4585
|
+
)
|
|
4586
|
+
];
|
|
4587
|
+
}
|
|
4588
|
+
|
|
4589
|
+
// src/scrap/report/blocks/comparison.ts
|
|
4590
|
+
function comparisonLines(metric) {
|
|
4591
|
+
if (!metric.comparison) {
|
|
4592
|
+
return [];
|
|
4593
|
+
}
|
|
4594
|
+
return [
|
|
4595
|
+
` compare: ${metric.comparison.verdict} avg\u0394=${metric.comparison.averageScoreDelta} max\u0394=${metric.comparison.maxScoreDelta} extract\u0394=${metric.comparison.extractionPressureDelta} matrix\u0394=${metric.comparison.coverageMatrixDelta} dup\u0394=${metric.comparison.harmfulDuplicationDelta} helper\u0394=${metric.comparison.helperHiddenDelta}`
|
|
4596
|
+
];
|
|
4597
|
+
}
|
|
4598
|
+
|
|
4599
|
+
// src/scrap/report/blocks/examples.ts
|
|
4600
|
+
function worstExampleLines(metric) {
|
|
4601
|
+
if (metric.worstExamples.length === 0) {
|
|
4602
|
+
return [];
|
|
4603
|
+
}
|
|
4604
|
+
return [
|
|
4605
|
+
" worst examples:",
|
|
4606
|
+
...metric.worstExamples.map(
|
|
4607
|
+
(example) => ` - ${example.name} (L${example.startLine}-L${example.endLine}) score=${example.score} assertions=${example.assertionCount} branches=${example.branchCount} mocks=${example.mockCount} setup=${example.setupLineCount} dupes=${example.duplicateSetupGroupSize} helpers=${example.helperCallCount} hidden=${example.helperHiddenLineCount}`
|
|
4608
|
+
)
|
|
4609
|
+
];
|
|
4610
|
+
}
|
|
4611
|
+
function verboseExampleLines(metric) {
|
|
4612
|
+
return [
|
|
4613
|
+
" verbose examples:",
|
|
4614
|
+
...metric.worstExamples.map(
|
|
4615
|
+
(example) => ` - ${example.name} tableDriven=${example.tableDriven} setupDepth=${example.setupDepth} tempResources=${example.tempResourceCount} snapshots=${example.snapshotCount ?? 0} waits=${example.asyncWaitCount ?? 0} fakeTimers=${example.fakeTimerCount ?? 0} moduleMocks=${example.moduleMockCount ?? 0} envMutations=${example.envMutationCount ?? 0} concurrent=${example.concurrencyCount ?? 0} typeOnly=${example.typeOnlyAssertionCount ?? 0}`
|
|
4616
|
+
)
|
|
4617
|
+
];
|
|
4618
|
+
}
|
|
4619
|
+
|
|
4620
|
+
// src/scrap/report/blocks/recommendations.ts
|
|
4621
|
+
function recommendationLines(metric) {
|
|
4622
|
+
if ((metric.recommendations?.length ?? 0) === 0) {
|
|
4623
|
+
return [];
|
|
4624
|
+
}
|
|
4625
|
+
return [
|
|
4626
|
+
" recommendations:",
|
|
4627
|
+
...(metric.recommendations ?? []).map(
|
|
4628
|
+
(recommendation) => ` - ${recommendation.kind} confidence=${recommendation.confidence} ${recommendation.message}`
|
|
4629
|
+
)
|
|
4630
|
+
];
|
|
4631
|
+
}
|
|
4632
|
+
|
|
4633
|
+
// src/scrap/report/summary.ts
|
|
4634
|
+
import { relative as relative8 } from "path";
|
|
4635
|
+
function summaryCount(value) {
|
|
4636
|
+
return value ?? 0;
|
|
4637
|
+
}
|
|
4638
|
+
function coreSummaryLines(metric) {
|
|
4639
|
+
return [
|
|
4640
|
+
` mode: ${metric.remediationMode}`,
|
|
4641
|
+
` examples: ${metric.exampleCount}`,
|
|
4642
|
+
` avg/max: ${metric.averageScore} / ${metric.maxScore}`,
|
|
4643
|
+
` actionability: ${metric.aiActionability ?? "LEAVE_ALONE"}`
|
|
4644
|
+
];
|
|
4645
|
+
}
|
|
4646
|
+
function duplicationSummaryLines(metric) {
|
|
4647
|
+
return [
|
|
4648
|
+
` zero-assertion: ${metric.zeroAssertionExampleCount}`,
|
|
4649
|
+
` low-assertion: ${metric.lowAssertionExampleCount}`,
|
|
4650
|
+
` branching: ${metric.branchingExampleCount}`,
|
|
4651
|
+
` duplicate-setup: ${metric.duplicateSetupExampleCount}`,
|
|
4652
|
+
` fixture-duplication: ${metric.fixtureDuplicationScore ?? 0}`,
|
|
4653
|
+
` literal-duplication: ${metric.literalDuplicationScore ?? 0}`,
|
|
4654
|
+
` helper-hidden: ${metric.helperHiddenExampleCount}`,
|
|
4655
|
+
` coverage-matrix: ${metric.coverageMatrixCandidateCount ?? 0}`,
|
|
4656
|
+
` extraction-pressure: ${metric.extractionPressureScore ?? 0}`
|
|
4657
|
+
];
|
|
4658
|
+
}
|
|
4659
|
+
function cohesionSummaryLines(metric) {
|
|
4660
|
+
return [
|
|
4661
|
+
` subjects: ${metric.distinctSubjectCount ?? 0}`,
|
|
4662
|
+
` subject-overlap: ${metric.averageSubjectOverlap ?? 0}`,
|
|
4663
|
+
` shape-diversity: ${metric.exampleShapeDiversity ?? 0}`,
|
|
4664
|
+
` fixture-diversity: ${metric.fixtureShapeDiversity ?? 0}`
|
|
4665
|
+
];
|
|
4666
|
+
}
|
|
4667
|
+
function vitestSignalCounts(metric) {
|
|
4668
|
+
const snapshots = summaryCount(metric.snapshotExampleCount);
|
|
4669
|
+
const waits = summaryCount(metric.asyncWaitExampleCount);
|
|
4670
|
+
const fakeTimers = summaryCount(metric.fakeTimerExampleCount);
|
|
4671
|
+
const moduleMocks = summaryCount(metric.moduleMockExampleCount);
|
|
4672
|
+
const envMutations = summaryCount(metric.envMutationExampleCount);
|
|
4673
|
+
const concurrent = summaryCount(metric.concurrencyExampleCount);
|
|
4674
|
+
const typeOnly = summaryCount(metric.typeOnlyAssertionExampleCount);
|
|
4675
|
+
const rtlRender = summaryCount(metric.rtlRenderExampleCount);
|
|
4676
|
+
const rtlQueryHeavy = summaryCount(metric.rtlQueryHeavyExampleCount);
|
|
4677
|
+
const rtlMutations = summaryCount(metric.rtlMutationExampleCount);
|
|
4678
|
+
return ` vitest-signals: snapshots=${snapshots} waits=${waits} fake-timers=${fakeTimers} module-mocks=${moduleMocks} env/global=${envMutations} concurrent=${concurrent} type-only=${typeOnly} rtl-renders=${rtlRender} rtl-query-heavy=${rtlQueryHeavy} rtl-mutations=${rtlMutations}`;
|
|
4679
|
+
}
|
|
4680
|
+
function vitestSummaryLines(metric) {
|
|
4681
|
+
return [
|
|
4682
|
+
vitestSignalCounts(metric),
|
|
4683
|
+
` temp-resources: ${summaryCount(metric.tempResourceExampleCount)}`,
|
|
4684
|
+
` validation-issues: ${metric.validationIssues?.length ?? 0}`
|
|
4685
|
+
];
|
|
4686
|
+
}
|
|
4687
|
+
function summaryLines4(metric, repoRoot) {
|
|
4688
|
+
return [
|
|
4689
|
+
`
|
|
4690
|
+
${relative8(repoRoot, metric.filePath)}`,
|
|
4691
|
+
...coreSummaryLines(metric),
|
|
4692
|
+
...duplicationSummaryLines(metric),
|
|
4693
|
+
...cohesionSummaryLines(metric),
|
|
4694
|
+
...vitestSummaryLines(metric)
|
|
4695
|
+
];
|
|
4696
|
+
}
|
|
4697
|
+
|
|
4698
|
+
// src/scrap/report/blocks/validation.ts
|
|
4699
|
+
function validationLines(metric) {
|
|
4700
|
+
if ((metric.validationIssues?.length ?? 0) === 0) {
|
|
4701
|
+
return [];
|
|
4702
|
+
}
|
|
4703
|
+
return [
|
|
4704
|
+
" validation:",
|
|
4705
|
+
...(metric.validationIssues ?? []).map(
|
|
4706
|
+
(issue2) => ` - [${issue2.kind}] L${issue2.line} ${issue2.message}`
|
|
4707
|
+
)
|
|
4708
|
+
];
|
|
4709
|
+
}
|
|
4710
|
+
|
|
4711
|
+
// src/scrap/report/format.ts
|
|
4712
|
+
function logLines4(lines) {
|
|
4713
|
+
for (const line of lines) {
|
|
4714
|
+
console.log(line);
|
|
4715
|
+
}
|
|
4716
|
+
}
|
|
4717
|
+
function reportScrap(metrics, repoRoot, options = {}) {
|
|
4718
|
+
if (metrics.length === 0) {
|
|
4719
|
+
console.log("\nNo test files found for SCRAP analysis.\n");
|
|
4720
|
+
return;
|
|
4721
|
+
}
|
|
4722
|
+
for (const metric of metrics) {
|
|
4723
|
+
logLines4(summaryLines4(metric, repoRoot));
|
|
4724
|
+
logLines4(comparisonLines(metric));
|
|
4725
|
+
logLines4(validationLines(metric));
|
|
4726
|
+
logLines4(recommendationLines(metric));
|
|
4727
|
+
logLines4(hotBlockLines(metric));
|
|
4728
|
+
logLines4(worstExampleLines(metric));
|
|
4729
|
+
if (options.verbose) {
|
|
4730
|
+
logLines4(verboseExampleLines(metric));
|
|
4731
|
+
}
|
|
4732
|
+
}
|
|
4733
|
+
}
|
|
4734
|
+
|
|
4735
|
+
// src/scrap/baseline.ts
|
|
4736
|
+
import { mkdirSync as mkdirSync3, writeFileSync as writeFileSync3 } from "fs";
|
|
4737
|
+
import { join as join17 } from "path";
|
|
4738
|
+
function baselinePathFor2(targetRelativePath) {
|
|
4739
|
+
const reportKey = sanitizeReportKey(targetRelativePath === "." ? "repo" : targetRelativePath);
|
|
4740
|
+
return resolveReportPath(REPO_ROOT, "scrap", `${reportKey}.json`);
|
|
4741
|
+
}
|
|
4742
|
+
function baseline(targetRelativePath, metrics) {
|
|
4743
|
+
const baselinePath = baselinePathFor2(targetRelativePath);
|
|
4744
|
+
mkdirSync3(join17(baselinePath, ".."), { recursive: true });
|
|
4745
|
+
writeFileSync3(baselinePath, JSON.stringify(metrics, null, 2));
|
|
4746
|
+
}
|
|
4747
|
+
|
|
4748
|
+
// src/scrap/command.ts
|
|
4749
|
+
var DEFAULT_DEPENDENCIES5 = {
|
|
4750
|
+
analyzeScrap,
|
|
4751
|
+
reportScrap,
|
|
4752
|
+
resolveQualityTarget,
|
|
4753
|
+
setExitCode: (code) => {
|
|
4754
|
+
process.exitCode = code;
|
|
4755
|
+
}
|
|
4756
|
+
};
|
|
4757
|
+
function runScrapCli(rawArgs, dependencies = DEFAULT_DEPENDENCIES5) {
|
|
4758
|
+
const args2 = cleanCliArgs(rawArgs);
|
|
4759
|
+
const target = dependencies.resolveQualityTarget(REPO_ROOT, parseTargetArg(args2, ["--compare", "--policy"]));
|
|
4760
|
+
const comparePath = flagValue(args2, "--compare");
|
|
4761
|
+
const verbose = args2.includes("--verbose");
|
|
4762
|
+
const writeBaseline = args2.includes("--write-baseline");
|
|
4763
|
+
const policy = resolveScrapPolicy(args2);
|
|
4764
|
+
let metrics = dependencies.analyzeScrap(target);
|
|
4765
|
+
if (comparePath) {
|
|
4766
|
+
metrics = applyBaselineComparison(metrics, comparePath);
|
|
4767
|
+
}
|
|
4768
|
+
if (writeBaseline) {
|
|
4769
|
+
baseline(target.relativePath, metrics);
|
|
4770
|
+
}
|
|
4771
|
+
const policyFailure = hasPolicyViolations(metrics, policy);
|
|
4772
|
+
const failureMessage = policyFailureMessage(policy);
|
|
4773
|
+
if (args2.includes("--json")) {
|
|
4774
|
+
console.log(JSON.stringify(metrics, null, 2));
|
|
4775
|
+
if (policyFailure) {
|
|
4776
|
+
dependencies.setExitCode(1);
|
|
4777
|
+
}
|
|
4778
|
+
return;
|
|
4779
|
+
}
|
|
4780
|
+
dependencies.reportScrap(metrics, REPO_ROOT, { verbose });
|
|
4781
|
+
if (policyFailure && failureMessage) {
|
|
4782
|
+
console.error(failureMessage);
|
|
4783
|
+
dependencies.setExitCode(1);
|
|
4784
|
+
}
|
|
4785
|
+
}
|
|
4786
|
+
|
|
4787
|
+
// src/cli/main.ts
|
|
4788
|
+
var COMMANDS = {
|
|
4789
|
+
boundaries: runBoundariesCli,
|
|
4790
|
+
crap: runCrapCli,
|
|
4791
|
+
init: runInitCli,
|
|
4792
|
+
mutate: runMutationCli,
|
|
4793
|
+
organize: runOrganizeCli,
|
|
4794
|
+
reachability: runReachabilityCli,
|
|
4795
|
+
scrap: runScrapCli
|
|
4796
|
+
};
|
|
4797
|
+
function printHelp() {
|
|
4798
|
+
console.log(`quality-tools <command> [target] [flags]
|
|
4799
|
+
|
|
4800
|
+
Commands:
|
|
4801
|
+
init Create a starter quality.config.json
|
|
4802
|
+
organize Check folder structure, naming, and cohesion
|
|
4803
|
+
boundaries Check package/layer boundaries
|
|
4804
|
+
reachability Check dead surfaces and dead ends
|
|
4805
|
+
crap Check complexity and coverage risk
|
|
4806
|
+
mutate Run mutation testing through the configured runner
|
|
4807
|
+
scrap Check test structure and refactor pressure
|
|
4808
|
+
`);
|
|
4809
|
+
}
|
|
4810
|
+
var [command, ...args] = cleanCliArgs(process.argv.slice(2));
|
|
4811
|
+
if (!command || command === "--help" || command === "-h") {
|
|
4812
|
+
printHelp();
|
|
4813
|
+
process.exit(0);
|
|
4814
|
+
}
|
|
4815
|
+
var run = COMMANDS[command];
|
|
4816
|
+
if (!run) {
|
|
4817
|
+
console.error(`Unknown quality-tools command: ${command}`);
|
|
4818
|
+
printHelp();
|
|
4819
|
+
process.exit(1);
|
|
4820
|
+
}
|
|
4821
|
+
try {
|
|
4822
|
+
await run(args);
|
|
4823
|
+
} catch (error) {
|
|
4824
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
4825
|
+
process.exit(1);
|
|
4826
|
+
}
|