@ttt-productions/repo-guards 0.1.22 → 0.1.23
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/dist/modules/import-paths/guard.d.ts.map +1 -1
- package/dist/modules/import-paths/guard.js +6 -5
- package/dist/modules/import-paths/guard.js.map +1 -1
- package/dist/modules/repo/e2e-actions.d.ts +7 -2
- package/dist/modules/repo/e2e-actions.d.ts.map +1 -1
- package/dist/modules/repo/e2e-actions.js +163 -54
- package/dist/modules/repo/e2e-actions.js.map +1 -1
- package/dist/node/tooling/vitest-report.d.ts +1 -0
- package/dist/node/tooling/vitest-report.d.ts.map +1 -1
- package/dist/node/tooling/vitest-report.js +14 -1
- package/dist/node/tooling/vitest-report.js.map +1 -1
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"guard.d.ts","sourceRoot":"","sources":["../../../src/modules/import-paths/guard.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAS,MAAM,sBAAsB,CAAC;AAE9D,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,4BAA4B,CAAC;AAKzE,MAAM,WAAW,eAAe;IAC9B,aAAa,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;IAChD,UAAU,EAAE,aAAa,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,GAAG,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;CAC1D;
|
|
1
|
+
{"version":3,"file":"guard.d.ts","sourceRoot":"","sources":["../../../src/modules/import-paths/guard.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAS,MAAM,sBAAsB,CAAC;AAE9D,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,4BAA4B,CAAC;AAKzE,MAAM,WAAW,eAAe;IAC9B,aAAa,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;IAChD,UAAU,EAAE,aAAa,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,GAAG,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;CAC1D;AAuCD,eAAO,MAAM,eAAe,EAAE,sBAAsB,CAAC,eAAe,CAiCnE,CAAC;AAEF,wBAAgB,kBAAkB,CAChC,MAAM,EAAE,UAAU,EAClB,KAAK,EAAE,eAAe,GACrB,KAAK,CAAC;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,EAAE,CAAA;CAAE,CAAC,CAM3C"}
|
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
import { isTestPath, isUnderRoot, kindOfFile } from '../../node/layout.js';
|
|
1
|
+
import { declaredRoots, isTestPath, isUnderRoot, kindOfFile } from '../../node/layout.js';
|
|
2
2
|
import { importAliasesProblem, reviewedProblems } from '../../support/facts.js';
|
|
3
3
|
import { analyzeImportPaths, createImportPathProfile, findUnusedIndexMembers } from './analyzer.js';
|
|
4
4
|
import { REPOSITORY_GUARD_TIMEOUT_MS } from '../../support/timeouts.js';
|
|
5
5
|
const SCOPE = {
|
|
6
|
-
roots: ['app', 'functions'],
|
|
6
|
+
roots: ['app', 'functions', 'e2e', 'hostedE2e', 'scripts', 'workers'],
|
|
7
7
|
kinds: ['code'],
|
|
8
8
|
tests: true,
|
|
9
|
-
optionalRoots: ['functions'],
|
|
9
|
+
optionalRoots: ['functions', 'e2e', 'hostedE2e', 'scripts', 'workers'],
|
|
10
10
|
};
|
|
11
11
|
function readModules(files) {
|
|
12
12
|
const modules = new Map();
|
|
@@ -15,8 +15,9 @@ function readModules(files) {
|
|
|
15
15
|
return modules;
|
|
16
16
|
}
|
|
17
17
|
function analyzedRoots(reader) {
|
|
18
|
-
|
|
19
|
-
|
|
18
|
+
return declaredRoots(reader.layout)
|
|
19
|
+
.filter(({ role }) => role !== 'coreDist')
|
|
20
|
+
.map(({ root }) => root.path);
|
|
20
21
|
}
|
|
21
22
|
function excludedModuleExists(reader, p) {
|
|
22
23
|
return ((reader.layout.exclusions ?? []).some((e) => isUnderRoot(p, e.path)) &&
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"guard.js","sourceRoot":"","sources":["../../../src/modules/import-paths/guard.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,UAAU,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,sBAAsB,CAAC;
|
|
1
|
+
{"version":3,"file":"guard.js","sourceRoot":"","sources":["../../../src/modules/import-paths/guard.ts"],"names":[],"mappings":"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","sourcesContent":["import type { Finding, GuardContext } from '../../guard.js';\nimport type { RepoReader, Scope } from '../../node/reader.js';\nimport { declaredRoots, isTestPath, isUnderRoot, kindOfFile } from '../../node/layout.js';\nimport type { GenericGuardDefinition } from '../../node/suite/config.js';\nimport { importAliasesProblem, reviewedProblems } from '../../support/facts.js';\nimport { analyzeImportPaths, createImportPathProfile, findUnusedIndexMembers } from './analyzer.js';\nimport { REPOSITORY_GUARD_TIMEOUT_MS } from '../../support/timeouts.js';\n\nexport interface ImportPathFacts {\n importAliases: Readonly<Record<string, string>>;\n exceptions: ReadonlyArray<{ file: string; why: string }>;\n}\n\nconst SCOPE: Scope = {\n roots: ['app', 'functions', 'e2e', 'hostedE2e', 'scripts', 'workers'],\n kinds: ['code'],\n tests: true,\n optionalRoots: ['functions', 'e2e', 'hostedE2e', 'scripts', 'workers'],\n};\n\nfunction readModules(files: ReturnType<RepoReader['files']>) {\n const modules = new Map<string, { text: string; sourceFile: import('typescript').SourceFile }>();\n for (const file of files)\n modules.set(file.path, { text: file.text, sourceFile: file.sourceFile });\n return modules;\n}\n\nfunction analyzedRoots(reader: RepoReader) {\n return declaredRoots(reader.layout)\n .filter(({ role }) => role !== 'coreDist')\n .map(({ root }) => root.path);\n}\n\nfunction excludedModuleExists(reader: RepoReader, p: string): boolean {\n return (\n (reader.layout.exclusions ?? []).some((e) => isUnderRoot(p, e.path)) &&\n kindOfFile(p.slice(p.lastIndexOf('/') + 1)) === 'code' &&\n reader.exists(p)\n );\n}\n\nfunction profileFor(reader: RepoReader, facts: ImportPathFacts) {\n return createImportPathProfile({\n importAliases: { ...facts.importAliases },\n exceptions: facts.exceptions.map((e) => ({ ...e })),\n isTestFile: (p: string) => isTestPath(reader.layout, p),\n isExcludedModule: (p: string) => excludedModuleExists(reader, p),\n });\n}\n\nexport const importPathGuard: GenericGuardDefinition<ImportPathFacts> = {\n id: 'importPaths',\n absentWhen: false,\n factProblems(facts) {\n const problems: string[] = [];\n importAliasesProblem(facts?.importAliases, 'importAliases', problems);\n reviewedProblems(facts?.exceptions, 'exceptions', problems, { key: 'file' });\n return problems;\n },\n build(facts) {\n return [\n {\n title: 'every definition has exactly one import path',\n description:\n 'A module that is not a folder index never re-exports; a folder index forwards only files inside its folder; ' +\n 'code outside a folder that has an index imports through it, never past it; code inside a folder imports its ' +\n 'siblings directly, never through an index of a folder that contains it. Import the owner — the sibling file or the package.',\n timeoutMs: REPOSITORY_GUARD_TIMEOUT_MS,\n run(context: GuardContext): Finding[] {\n const files = context.files(SCOPE);\n const result = analyzeImportPaths({\n files: readModules(files),\n roots: analyzedRoots(context.reader),\n profile: profileFor(context.reader, facts),\n });\n return result.violations.map((v) => ({\n file: v.file,\n detail: `${v.kind.replace(/-/g, ' ')}: ${v.detail}`,\n }));\n },\n },\n ];\n },\n};\n\nexport function unusedIndexMembers(\n reader: RepoReader,\n facts: ImportPathFacts,\n): Array<{ file: string; unused: string[] }> {\n return findUnusedIndexMembers({\n files: readModules(reader.files(SCOPE)),\n roots: analyzedRoots(reader),\n profile: profileFor(reader, facts),\n });\n}\n"]}
|
|
@@ -1,8 +1,13 @@
|
|
|
1
|
+
import ts from 'typescript';
|
|
1
2
|
import type { GenericGuardDefinition } from '../../node/suite/config.js';
|
|
2
3
|
import { type FileView } from '../../reader/view.js';
|
|
3
4
|
import { type AnchoredEntry, type Occurrence } from '../../support/anchored-review.js';
|
|
4
|
-
export
|
|
5
|
-
|
|
5
|
+
export interface ActionReach {
|
|
6
|
+
actionIn(file: FileView, root: ts.Node): string[] | undefined;
|
|
7
|
+
}
|
|
8
|
+
export declare function createActionReach(files: readonly FileView[], helpers?: readonly string[]): ActionReach;
|
|
9
|
+
export declare function retriedActions(source: FileView | string, helpers?: readonly string[], reach?: ActionReach): Occurrence[];
|
|
10
|
+
export declare function swallowedActions(source: FileView | string, helpers?: readonly string[], reach?: ActionReach): Occurrence[];
|
|
6
11
|
export interface E2eActionFacts {
|
|
7
12
|
actionHelpers: readonly string[];
|
|
8
13
|
reviewed: readonly AnchoredEntry[];
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"e2e-actions.d.ts","sourceRoot":"","sources":["../../../src/modules/repo/e2e-actions.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"e2e-actions.d.ts","sourceRoot":"","sources":["../../../src/modules/repo/e2e-actions.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,YAAY,CAAC;AAC5B,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,4BAA4B,CAAC;AAEzE,OAAO,EAAc,KAAK,QAAQ,EAAE,MAAM,sBAAsB,CAAC;AAGjE,OAAO,EAGL,KAAK,aAAa,EAClB,KAAK,UAAU,EAChB,MAAM,kCAAkC,CAAC;AAmI1C,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,IAAI,EAAE,QAAQ,EAAE,IAAI,EAAE,EAAE,CAAC,IAAI,GAAG,MAAM,EAAE,GAAG,SAAS,CAAC;CAC/D;AAED,wBAAgB,iBAAiB,CAC/B,KAAK,EAAE,SAAS,QAAQ,EAAE,EAC1B,OAAO,GAAE,SAAS,MAAM,EAAO,GAC9B,WAAW,CAgDb;AAcD,wBAAgB,cAAc,CAC5B,MAAM,EAAE,QAAQ,GAAG,MAAM,EACzB,OAAO,GAAE,SAAS,MAAM,EAAO,EAC/B,KAAK,CAAC,EAAE,WAAW,GAClB,UAAU,EAAE,CA4Bd;AAmBD,wBAAgB,gBAAgB,CAC9B,MAAM,EAAE,QAAQ,GAAG,MAAM,EACzB,OAAO,GAAE,SAAS,MAAM,EAAO,EAC/B,KAAK,CAAC,EAAE,WAAW,GAClB,UAAU,EAAE,CA8Cd;AAWD,MAAM,WAAW,cAAc;IAC7B,aAAa,EAAE,SAAS,MAAM,EAAE,CAAC;IACjC,QAAQ,EAAE,SAAS,aAAa,EAAE,CAAC;CACpC;AAUD,eAAO,MAAM,oBAAoB,EAAE,sBAAsB,CAAC,cAAc,CA8BvE,CAAC;AAEF,eAAO,MAAM,oBAAoB,EAAE,sBAAsB,CAAC,cAAc,CA6BvE,CAAC"}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import ts from 'typescript';
|
|
2
2
|
import { viewOfText } from '../../reader/view.js';
|
|
3
3
|
import { calleeName, forEachNode } from '../../support/ast.js';
|
|
4
|
+
import { resolveRelative } from '../../support/paths.js';
|
|
4
5
|
import { anchoredFindings, anchoredProblems, } from '../../support/anchored-review.js';
|
|
5
6
|
import { namesProblem, objectProblem } from '../../support/facts.js';
|
|
6
7
|
import { REPOSITORY_GUARD_TIMEOUT_MS } from '../../support/timeouts.js';
|
|
@@ -66,29 +67,141 @@ function retriedCallbacks(sf) {
|
|
|
66
67
|
});
|
|
67
68
|
return callbacks;
|
|
68
69
|
}
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
const
|
|
70
|
+
const isRepeatingLoop = (node) => ts.isForStatement(node) || ts.isWhileStatement(node) || ts.isDoStatement(node);
|
|
71
|
+
function functionBodiesOf(sf) {
|
|
72
|
+
const bodies = new Map();
|
|
73
|
+
const add = (name, body) => bodies.set(name, [...(bodies.get(name) ?? []), body]);
|
|
74
|
+
forEachNode(sf, (node) => {
|
|
75
|
+
if (ts.isFunctionDeclaration(node) && node.name && node.body)
|
|
76
|
+
add(node.name.text, node.body);
|
|
77
|
+
if (ts.isVariableDeclaration(node) &&
|
|
78
|
+
ts.isIdentifier(node.name) &&
|
|
79
|
+
node.initializer &&
|
|
80
|
+
isFunctionNode(node.initializer))
|
|
81
|
+
add(node.name.text, node.initializer.body);
|
|
82
|
+
});
|
|
83
|
+
return bodies;
|
|
84
|
+
}
|
|
85
|
+
const SOURCE_EXTENSIONS = ['.ts', '.tsx', '.mts', '.cts', '.js', '.jsx', '.mjs', '.cjs'];
|
|
86
|
+
function moduleCandidates(base) {
|
|
87
|
+
const stem = base.replace(/\.(?:[cm]?js|jsx)$/, '');
|
|
88
|
+
return [
|
|
89
|
+
base,
|
|
90
|
+
...SOURCE_EXTENSIONS.map((ext) => stem + ext),
|
|
91
|
+
...SOURCE_EXTENSIONS.map((ext) => `${base}/index${ext}`),
|
|
92
|
+
];
|
|
93
|
+
}
|
|
94
|
+
// Where an imported function lives: the file and the name it is declared under there.
|
|
95
|
+
function namedImportsOf(file, paths) {
|
|
96
|
+
const imports = new Map();
|
|
97
|
+
for (const statement of file.sourceFile.statements) {
|
|
98
|
+
if (!ts.isImportDeclaration(statement) || !ts.isStringLiteral(statement.moduleSpecifier))
|
|
99
|
+
continue;
|
|
100
|
+
const bindings = statement.importClause?.namedBindings;
|
|
101
|
+
if (!bindings || !ts.isNamedImports(bindings))
|
|
102
|
+
continue;
|
|
103
|
+
const base = resolveRelative(file.path, statement.moduleSpecifier.text)?.path;
|
|
104
|
+
const target = base && moduleCandidates(base).find((candidate) => paths.has(candidate));
|
|
105
|
+
if (!target)
|
|
106
|
+
continue;
|
|
107
|
+
for (const element of bindings.elements)
|
|
108
|
+
imports.set(element.name.text, {
|
|
109
|
+
path: target,
|
|
110
|
+
name: (element.propertyName ?? element.name).text,
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
return imports;
|
|
114
|
+
}
|
|
115
|
+
export function createActionReach(files, helpers = []) {
|
|
72
116
|
const actions = actionNamesOf(helpers);
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
117
|
+
const byPath = new Map(files.map((file) => [file.path, file]));
|
|
118
|
+
const paths = new Set(byPath.keys());
|
|
119
|
+
const bodiesCache = new Map();
|
|
120
|
+
const importsCache = new Map();
|
|
121
|
+
const bodiesOf = (file) => {
|
|
122
|
+
let bodies = bodiesCache.get(file.path);
|
|
123
|
+
if (!bodies)
|
|
124
|
+
bodiesCache.set(file.path, (bodies = functionBodiesOf(file.sourceFile)));
|
|
125
|
+
return bodies;
|
|
126
|
+
};
|
|
127
|
+
const importsOf = (file) => {
|
|
128
|
+
let imports = importsCache.get(file.path);
|
|
129
|
+
if (!imports)
|
|
130
|
+
importsCache.set(file.path, (imports = namedImportsOf(file, paths)));
|
|
131
|
+
return imports;
|
|
132
|
+
};
|
|
133
|
+
function search(file, root, seen) {
|
|
134
|
+
let found;
|
|
135
|
+
forEachNode(root, (node) => {
|
|
136
|
+
if (found || !ts.isCallExpression(node))
|
|
137
|
+
return;
|
|
138
|
+
const name = calleeName(node);
|
|
139
|
+
if (!name)
|
|
140
|
+
return;
|
|
141
|
+
if (actions.has(name)) {
|
|
142
|
+
found = [name];
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
if (!ts.isIdentifier(node.expression))
|
|
146
|
+
return;
|
|
147
|
+
const imported = importsOf(file).get(name);
|
|
148
|
+
const local = bodiesOf(file).get(name);
|
|
149
|
+
const owner = local ? file : imported ? byPath.get(imported.path) : undefined;
|
|
150
|
+
const declared = local ? name : imported?.name;
|
|
151
|
+
if (!owner || !declared)
|
|
152
|
+
return;
|
|
153
|
+
const key = `${owner.path}#${declared}`;
|
|
154
|
+
if (seen.has(key))
|
|
155
|
+
return;
|
|
156
|
+
seen.add(key);
|
|
157
|
+
for (const body of bodiesOf(owner).get(declared) ?? []) {
|
|
158
|
+
const inner = search(owner, body, seen);
|
|
159
|
+
if (inner) {
|
|
160
|
+
found = [name, ...inner];
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
80
163
|
}
|
|
81
164
|
});
|
|
82
|
-
return
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
165
|
+
return found;
|
|
166
|
+
}
|
|
167
|
+
return { actionIn: (file, root) => search(file, root, new Set()) };
|
|
168
|
+
}
|
|
169
|
+
// `['openWork', 'goto']` reads "goto() through openWork".
|
|
170
|
+
function describeAction(chain) {
|
|
171
|
+
const action = `${chain[chain.length - 1]}()`;
|
|
172
|
+
return chain.length > 1 ? `${action} through ${chain.slice(0, -1).join(' → ')}` : action;
|
|
173
|
+
}
|
|
174
|
+
const within = (inner, outer) => inner !== outer && inner.pos >= outer.pos && inner.end <= outer.end;
|
|
175
|
+
const viewOf = (source) => typeof source === 'string' ? viewOfText('sample.spec.ts', source) : source;
|
|
176
|
+
export function retriedActions(source, helpers = [], reach) {
|
|
177
|
+
const file = viewOf(source);
|
|
178
|
+
const sf = file.sourceFile;
|
|
179
|
+
const reachFrom = reach ?? createActionReach([file], helpers);
|
|
180
|
+
const loops = [];
|
|
181
|
+
forEachNode(sf, (node) => {
|
|
182
|
+
if (isRepeatingLoop(node))
|
|
183
|
+
loops.push(node);
|
|
184
|
+
});
|
|
185
|
+
const acting = [
|
|
186
|
+
...retriedCallbacks(sf).map((node) => ({
|
|
187
|
+
node,
|
|
188
|
+
body: node.body,
|
|
189
|
+
kind: 'a retry loop',
|
|
190
|
+
tail: ' until it passes',
|
|
191
|
+
})),
|
|
192
|
+
...loops.map((node) => ({ node, body: node, kind: 'a for / while / do loop', tail: '' })),
|
|
193
|
+
].flatMap((loop) => {
|
|
194
|
+
const chain = reachFrom.actionIn(file, loop.body);
|
|
195
|
+
return chain ? [{ ...loop, chain }] : [];
|
|
91
196
|
});
|
|
197
|
+
return acting
|
|
198
|
+
.filter((loop) => !acting.some((outer) => within(loop.node, outer.node)))
|
|
199
|
+
.sort((a, b) => a.node.getStart(sf) - b.node.getStart(sf))
|
|
200
|
+
.map((loop) => ({
|
|
201
|
+
start: loop.node.getStart(sf),
|
|
202
|
+
end: loop.node.getEnd(),
|
|
203
|
+
detail: `${loop.kind} repeats ${describeAction(loop.chain)}${loop.tail}`,
|
|
204
|
+
}));
|
|
92
205
|
}
|
|
93
206
|
function catchesRejection(handler) {
|
|
94
207
|
if (!isFunctionNode(handler))
|
|
@@ -100,21 +213,18 @@ function catchesRejection(handler) {
|
|
|
100
213
|
});
|
|
101
214
|
return !rethrows;
|
|
102
215
|
}
|
|
103
|
-
// The call a `.catch()` is chained onto: `x.click().catch(…)` → `click
|
|
104
|
-
function
|
|
216
|
+
// The call a `.catch()` is chained onto: `x.click().catch(…)` → the `click()` call.
|
|
217
|
+
function caughtCall(catchCall) {
|
|
105
218
|
if (!ts.isPropertyAccessExpression(catchCall.expression))
|
|
106
219
|
return undefined;
|
|
107
220
|
const receiver = catchCall.expression.expression;
|
|
108
221
|
const call = ts.isParenthesizedExpression(receiver) ? receiver.expression : receiver;
|
|
109
|
-
|
|
110
|
-
return undefined;
|
|
111
|
-
const name = calleeName(call);
|
|
112
|
-
return name && names.has(name) ? name : undefined;
|
|
222
|
+
return ts.isCallExpression(call) ? call : undefined;
|
|
113
223
|
}
|
|
114
|
-
export function swallowedActions(source, helpers = []) {
|
|
115
|
-
const file =
|
|
224
|
+
export function swallowedActions(source, helpers = [], reach) {
|
|
225
|
+
const file = viewOf(source);
|
|
116
226
|
const sf = file.sourceFile;
|
|
117
|
-
const
|
|
227
|
+
const reachFrom = reach ?? createActionReach([file], helpers);
|
|
118
228
|
const hits = [];
|
|
119
229
|
forEachNode(sf, (node) => {
|
|
120
230
|
if (ts.isCallExpression(node) &&
|
|
@@ -122,19 +232,22 @@ export function swallowedActions(source, helpers = []) {
|
|
|
122
232
|
node.expression.name.text === 'catch' &&
|
|
123
233
|
node.arguments.length === 1 &&
|
|
124
234
|
catchesRejection(node.arguments[0])) {
|
|
125
|
-
const
|
|
126
|
-
|
|
127
|
-
|
|
235
|
+
const call = caughtCall(node);
|
|
236
|
+
if (!call)
|
|
237
|
+
return;
|
|
238
|
+
const name = calleeName(call);
|
|
239
|
+
const chain = reachFrom.actionIn(file, call);
|
|
240
|
+
if (chain)
|
|
128
241
|
hits.push({
|
|
129
242
|
start: node.getStart(sf),
|
|
130
243
|
end: node.getEnd(),
|
|
131
|
-
detail: `a .catch() swallows a failed ${
|
|
244
|
+
detail: `a .catch() swallows a failed ${describeAction(chain)}`,
|
|
132
245
|
});
|
|
133
|
-
else if (
|
|
246
|
+
else if (name && STATE_PROBES.has(name))
|
|
134
247
|
hits.push({
|
|
135
248
|
start: node.getStart(sf),
|
|
136
249
|
end: node.getEnd(),
|
|
137
|
-
detail: `a .catch() turns a failed ${
|
|
250
|
+
detail: `a .catch() turns a failed ${name}() — an ambiguous locator included — into a value`,
|
|
138
251
|
});
|
|
139
252
|
}
|
|
140
253
|
if (ts.isTryStatement(node) && node.catchClause) {
|
|
@@ -145,24 +258,21 @@ export function swallowedActions(source, helpers = []) {
|
|
|
145
258
|
});
|
|
146
259
|
if (rethrows)
|
|
147
260
|
return;
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
if (!action && ts.isCallExpression(inner)) {
|
|
151
|
-
const name = calleeName(inner);
|
|
152
|
-
if (name && actions.has(name))
|
|
153
|
-
action = name;
|
|
154
|
-
}
|
|
155
|
-
});
|
|
156
|
-
if (action)
|
|
261
|
+
const chain = reachFrom.actionIn(file, node.tryBlock);
|
|
262
|
+
if (chain)
|
|
157
263
|
hits.push({
|
|
158
264
|
start: node.getStart(sf),
|
|
159
265
|
end: node.getEnd(),
|
|
160
|
-
detail: `a try/catch swallows a failed ${
|
|
266
|
+
detail: `a try/catch swallows a failed ${describeAction(chain)}`,
|
|
161
267
|
});
|
|
162
268
|
}
|
|
163
269
|
});
|
|
164
270
|
return hits;
|
|
165
271
|
}
|
|
272
|
+
function withReach(files, helpers, find) {
|
|
273
|
+
const reach = createActionReach(files, helpers);
|
|
274
|
+
return files.map((file) => ({ file, hits: find(file, reach) }));
|
|
275
|
+
}
|
|
166
276
|
function e2eActionProblems(facts) {
|
|
167
277
|
const problems = [];
|
|
168
278
|
if (!objectProblem(facts, 'the facts', problems))
|
|
@@ -180,16 +290,16 @@ export const retryLoopActionGuard = {
|
|
|
180
290
|
return [
|
|
181
291
|
{
|
|
182
292
|
title: 'no end-to-end retry loop repeats a user action',
|
|
183
|
-
description: 'A toPass() or expect.poll() callback that clicks, types, or navigates
|
|
184
|
-
'happens to work, so the failure a real user would hit passes as green
|
|
293
|
+
description: 'A toPass() or expect.poll() callback, or a for (;;) / while / do loop, that clicks, types, or navigates ' +
|
|
294
|
+
'repeats the action until it happens to work, so the failure a real user would hit passes as green ' +
|
|
295
|
+
'(RULE-STD-QUALITY-003). Walking a list is written for…of, which is not a repeat. Act once, ' +
|
|
185
296
|
'then assert with a web-first assertion; a loop that only re-reads state is fine. The actions are ' +
|
|
186
|
-
"Playwright's input and navigation calls plus the app's declared actionHelpers
|
|
297
|
+
"Playwright's input and navigation calls plus the app's declared actionHelpers, reached directly or through " +
|
|
298
|
+
'a plain call to a function the suite declares in the same file or imports from its own files. A loop that must act — ' +
|
|
187
299
|
'polling a background result by reloading — is a reviewed entry whose anchor names that one loop, with ' +
|
|
188
300
|
'its why; an entry that no longer matches a loop is stale.',
|
|
189
301
|
timeoutMs: REPOSITORY_GUARD_TIMEOUT_MS,
|
|
190
|
-
run: (context) => anchoredFindings(context
|
|
191
|
-
.files(E2E_SCOPE)
|
|
192
|
-
.map((file) => ({ file, hits: retriedActions(file, facts.actionHelpers) })), facts.reviewed, 'its anchor names no retry loop that acts'),
|
|
302
|
+
run: (context) => anchoredFindings(withReach(context.files(E2E_SCOPE), facts.actionHelpers, (file, reach) => retriedActions(file, facts.actionHelpers, reach)), facts.reviewed, 'its anchor names no retry loop that acts'),
|
|
193
303
|
},
|
|
194
304
|
];
|
|
195
305
|
},
|
|
@@ -203,16 +313,15 @@ export const swallowedActionGuard = {
|
|
|
203
313
|
return [
|
|
204
314
|
{
|
|
205
315
|
title: 'no end-to-end action or state read swallows its failure',
|
|
206
|
-
description: 'A .catch() or a try/catch that discards a failed click, fill, or navigation
|
|
316
|
+
description: 'A .catch() or a try/catch that discards a failed click, fill, or navigation — made directly or through a plain ' +
|
|
317
|
+
"call to one of the suite's own functions — lets the next step run on a " +
|
|
207
318
|
'page the action never reached, and one on isVisible(), isEnabled(), or isChecked() turns an ambiguous ' +
|
|
208
319
|
'(strict-mode) locator into a silent "no". Let the failure throw: an action whose outcome is uncertain ' +
|
|
209
320
|
"goes through the suite's outcome-verified helper, and a branch reads state with no catch. A catch that " +
|
|
210
321
|
'must stay is a reviewed entry whose anchor names that one occurrence, with its why; an entry that no ' +
|
|
211
322
|
'longer matches one is stale.',
|
|
212
323
|
timeoutMs: REPOSITORY_GUARD_TIMEOUT_MS,
|
|
213
|
-
run: (context) => anchoredFindings(context
|
|
214
|
-
.files(E2E_SCOPE)
|
|
215
|
-
.map((file) => ({ file, hits: swallowedActions(file, facts.actionHelpers) })), facts.reviewed, 'its anchor names no swallowed action or state read'),
|
|
324
|
+
run: (context) => anchoredFindings(withReach(context.files(E2E_SCOPE), facts.actionHelpers, (file, reach) => swallowedActions(file, facts.actionHelpers, reach)), facts.reviewed, 'its anchor names no swallowed action or state read'),
|
|
216
325
|
},
|
|
217
326
|
];
|
|
218
327
|
},
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"e2e-actions.js","sourceRoot":"","sources":["../../../src/modules/repo/e2e-actions.ts"],"names":[],"mappings":"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","sourcesContent":["import ts from 'typescript';\nimport type { GenericGuardDefinition } from '../../node/suite/config.js';\nimport type { Scope } from '../../node/reader.js';\nimport { viewOfText, type FileView } from '../../reader/view.js';\nimport { calleeName, forEachNode } from '../../support/ast.js';\nimport {\n anchoredFindings,\n anchoredProblems,\n type AnchoredEntry,\n type Occurrence,\n} from '../../support/anchored-review.js';\nimport { namesProblem, objectProblem } from '../../support/facts.js';\nimport { REPOSITORY_GUARD_TIMEOUT_MS } from '../../support/timeouts.js';\n\n// Playwright's locator and page operations that act on the page: input, and navigation.\nconst PLAYWRIGHT_ACTIONS: ReadonlySet<string> = new Set([\n 'check',\n 'clear',\n 'click',\n 'dblclick',\n 'dispatchEvent',\n 'dragTo',\n 'fill',\n 'goBack',\n 'goForward',\n 'goto',\n 'hover',\n 'press',\n 'pressSequentially',\n 'reload',\n 'selectOption',\n 'selectText',\n 'setChecked',\n 'setInputFiles',\n 'tap',\n 'type',\n 'uncheck',\n]);\n\n// Playwright's state reads: strict locators throw when a locator matches more than one element.\nconst STATE_PROBES: ReadonlySet<string> = new Set([\n 'isChecked',\n 'isDisabled',\n 'isEditable',\n 'isEnabled',\n 'isHidden',\n 'isVisible',\n]);\n\nconst E2E_SCOPE: Scope = {\n roots: ['e2e', 'hostedE2e'],\n kinds: ['code'],\n tests: true,\n optionalRoots: ['hostedE2e'],\n};\n\nconst actionNamesOf = (helpers: readonly string[]) => new Set([...PLAYWRIGHT_ACTIONS, ...helpers]);\n\nconst isFunctionNode = (node: ts.Node): node is ts.ArrowFunction | ts.FunctionExpression =>\n ts.isArrowFunction(node) || ts.isFunctionExpression(node);\n\n// `expect(fn).toPass(…)` and `expect.poll(fn)` retry `fn` until it passes.\nfunction retriedCallbacks(sf: ts.SourceFile): Array<ts.ArrowFunction | ts.FunctionExpression> {\n const callbacks: Array<ts.ArrowFunction | ts.FunctionExpression> = [];\n forEachNode(sf, (node) => {\n if (!ts.isCallExpression(node) || !ts.isPropertyAccessExpression(node.expression)) return;\n const method = node.expression.name.text;\n const target = node.expression.expression;\n if (method === 'toPass' && ts.isCallExpression(target)) {\n const fn = target.arguments[0];\n if (fn && isFunctionNode(fn) && calleeName(target)?.toLowerCase().endsWith('expect'))\n callbacks.push(fn);\n }\n if (method === 'poll' && calleeName(node) === 'poll') {\n const fn = node.arguments[0];\n if (fn && isFunctionNode(fn)) callbacks.push(fn);\n }\n });\n return callbacks;\n}\n\nexport function retriedActions(\n source: FileView | string,\n helpers: readonly string[] = [],\n): Occurrence[] {\n const file = typeof source === 'string' ? viewOfText('sample.spec.ts', source) : source;\n const sf = file.sourceFile;\n const actions = actionNamesOf(helpers);\n return retriedCallbacks(sf).flatMap((fn) => {\n let action: string | undefined;\n forEachNode(fn.body, (node) => {\n if (!action && ts.isCallExpression(node)) {\n const name = calleeName(node);\n if (name && actions.has(name)) action = name;\n }\n });\n return action\n ? [\n {\n start: fn.getStart(sf),\n end: fn.getEnd(),\n detail: `a retry loop repeats ${action}() until it passes`,\n },\n ]\n : [];\n });\n}\n\nfunction catchesRejection(handler: ts.Expression): boolean {\n if (!isFunctionNode(handler)) return true;\n let rethrows = false;\n forEachNode(handler.body, (node) => {\n if (ts.isThrowStatement(node)) rethrows = true;\n });\n return !rethrows;\n}\n\n// The call a `.catch()` is chained onto: `x.click().catch(…)` → `click`.\nfunction caughtCallOf(\n catchCall: ts.CallExpression,\n names: ReadonlySet<string>,\n): string | undefined {\n if (!ts.isPropertyAccessExpression(catchCall.expression)) return undefined;\n const receiver = catchCall.expression.expression;\n const call = ts.isParenthesizedExpression(receiver) ? receiver.expression : receiver;\n if (!ts.isCallExpression(call)) return undefined;\n const name = calleeName(call);\n return name && names.has(name) ? name : undefined;\n}\n\nexport function swallowedActions(\n source: FileView | string,\n helpers: readonly string[] = [],\n): Occurrence[] {\n const file = typeof source === 'string' ? viewOfText('sample.spec.ts', source) : source;\n const sf = file.sourceFile;\n const actions = actionNamesOf(helpers);\n const hits: Occurrence[] = [];\n forEachNode(sf, (node) => {\n if (\n ts.isCallExpression(node) &&\n ts.isPropertyAccessExpression(node.expression) &&\n node.expression.name.text === 'catch' &&\n node.arguments.length === 1 &&\n catchesRejection(node.arguments[0])\n ) {\n const action = caughtCallOf(node, actions);\n const probe = caughtCallOf(node, STATE_PROBES);\n if (action)\n hits.push({\n start: node.getStart(sf),\n end: node.getEnd(),\n detail: `a .catch() swallows a failed ${action}()`,\n });\n else if (probe)\n hits.push({\n start: node.getStart(sf),\n end: node.getEnd(),\n detail: `a .catch() turns a failed ${probe}() — an ambiguous locator included — into a value`,\n });\n }\n if (ts.isTryStatement(node) && node.catchClause) {\n let rethrows = false;\n forEachNode(node.catchClause.block, (inner) => {\n if (ts.isThrowStatement(inner)) rethrows = true;\n });\n if (rethrows) return;\n let action: string | undefined;\n forEachNode(node.tryBlock, (inner) => {\n if (!action && ts.isCallExpression(inner)) {\n const name = calleeName(inner);\n if (name && actions.has(name)) action = name;\n }\n });\n if (action)\n hits.push({\n start: node.getStart(sf),\n end: node.getEnd(),\n detail: `a try/catch swallows a failed ${action}()`,\n });\n }\n });\n return hits;\n}\n\nexport interface E2eActionFacts {\n actionHelpers: readonly string[];\n reviewed: readonly AnchoredEntry[];\n}\n\nfunction e2eActionProblems(facts: E2eActionFacts): string[] {\n const problems: string[] = [];\n if (!objectProblem(facts, 'the facts', problems)) return problems;\n namesProblem(facts.actionHelpers, 'actionHelpers', problems, { nonEmpty: false });\n anchoredProblems(facts.reviewed, 'reviewed', problems);\n return problems;\n}\n\nexport const retryLoopActionGuard: GenericGuardDefinition<E2eActionFacts> = {\n id: 'retryLoopActions',\n absentWhen: 'the repository has no end-to-end suite',\n absentOnlyWithoutRoot: 'e2e',\n factProblems: e2eActionProblems,\n build(facts) {\n return [\n {\n title: 'no end-to-end retry loop repeats a user action',\n description:\n 'A toPass() or expect.poll() callback that clicks, types, or navigates repeats the action until it ' +\n 'happens to work, so the failure a real user would hit passes as green (RULE-STD-QUALITY-003). Act once, ' +\n 'then assert with a web-first assertion; a loop that only re-reads state is fine. The actions are ' +\n \"Playwright's input and navigation calls plus the app's declared actionHelpers. A loop that must act — \" +\n 'polling a background result by reloading — is a reviewed entry whose anchor names that one loop, with ' +\n 'its why; an entry that no longer matches a loop is stale.',\n timeoutMs: REPOSITORY_GUARD_TIMEOUT_MS,\n run: (context) =>\n anchoredFindings(\n context\n .files(E2E_SCOPE)\n .map((file) => ({ file, hits: retriedActions(file, facts.actionHelpers) })),\n facts.reviewed,\n 'its anchor names no retry loop that acts',\n ),\n },\n ];\n },\n};\n\nexport const swallowedActionGuard: GenericGuardDefinition<E2eActionFacts> = {\n id: 'swallowedE2eActions',\n absentWhen: 'the repository has no end-to-end suite',\n absentOnlyWithoutRoot: 'e2e',\n factProblems: e2eActionProblems,\n build(facts) {\n return [\n {\n title: 'no end-to-end action or state read swallows its failure',\n description:\n 'A .catch() or a try/catch that discards a failed click, fill, or navigation lets the next step run on a ' +\n 'page the action never reached, and one on isVisible(), isEnabled(), or isChecked() turns an ambiguous ' +\n '(strict-mode) locator into a silent \"no\". Let the failure throw: an action whose outcome is uncertain ' +\n \"goes through the suite's outcome-verified helper, and a branch reads state with no catch. A catch that \" +\n 'must stay is a reviewed entry whose anchor names that one occurrence, with its why; an entry that no ' +\n 'longer matches one is stale.',\n timeoutMs: REPOSITORY_GUARD_TIMEOUT_MS,\n run: (context) =>\n anchoredFindings(\n context\n .files(E2E_SCOPE)\n .map((file) => ({ file, hits: swallowedActions(file, facts.actionHelpers) })),\n facts.reviewed,\n 'its anchor names no swallowed action or state read',\n ),\n },\n ];\n },\n};\n"]}
|
|
1
|
+
{"version":3,"file":"e2e-actions.js","sourceRoot":"","sources":["../../../src/modules/repo/e2e-actions.ts"],"names":[],"mappings":"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","sourcesContent":["import ts from 'typescript';\nimport type { GenericGuardDefinition } from '../../node/suite/config.js';\nimport type { Scope } from '../../node/reader.js';\nimport { viewOfText, type FileView } from '../../reader/view.js';\nimport { calleeName, forEachNode } from '../../support/ast.js';\nimport { resolveRelative } from '../../support/paths.js';\nimport {\n anchoredFindings,\n anchoredProblems,\n type AnchoredEntry,\n type Occurrence,\n} from '../../support/anchored-review.js';\nimport { namesProblem, objectProblem } from '../../support/facts.js';\nimport { REPOSITORY_GUARD_TIMEOUT_MS } from '../../support/timeouts.js';\n\n// Playwright's locator and page operations that act on the page: input, and navigation.\nconst PLAYWRIGHT_ACTIONS: ReadonlySet<string> = new Set([\n 'check',\n 'clear',\n 'click',\n 'dblclick',\n 'dispatchEvent',\n 'dragTo',\n 'fill',\n 'goBack',\n 'goForward',\n 'goto',\n 'hover',\n 'press',\n 'pressSequentially',\n 'reload',\n 'selectOption',\n 'selectText',\n 'setChecked',\n 'setInputFiles',\n 'tap',\n 'type',\n 'uncheck',\n]);\n\n// Playwright's state reads: strict locators throw when a locator matches more than one element.\nconst STATE_PROBES: ReadonlySet<string> = new Set([\n 'isChecked',\n 'isDisabled',\n 'isEditable',\n 'isEnabled',\n 'isHidden',\n 'isVisible',\n]);\n\nconst E2E_SCOPE: Scope = {\n roots: ['e2e', 'hostedE2e'],\n kinds: ['code'],\n tests: true,\n optionalRoots: ['hostedE2e'],\n};\n\nconst actionNamesOf = (helpers: readonly string[]) => new Set([...PLAYWRIGHT_ACTIONS, ...helpers]);\n\nconst isFunctionNode = (node: ts.Node): node is ts.ArrowFunction | ts.FunctionExpression =>\n ts.isArrowFunction(node) || ts.isFunctionExpression(node);\n\n// `expect(fn).toPass(…)` and `expect.poll(fn)` retry `fn` until it passes.\nfunction retriedCallbacks(sf: ts.SourceFile): Array<ts.ArrowFunction | ts.FunctionExpression> {\n const callbacks: Array<ts.ArrowFunction | ts.FunctionExpression> = [];\n forEachNode(sf, (node) => {\n if (!ts.isCallExpression(node) || !ts.isPropertyAccessExpression(node.expression)) return;\n const method = node.expression.name.text;\n const target = node.expression.expression;\n if (method === 'toPass' && ts.isCallExpression(target)) {\n const fn = target.arguments[0];\n if (fn && isFunctionNode(fn) && calleeName(target)?.toLowerCase().endsWith('expect'))\n callbacks.push(fn);\n }\n if (method === 'poll' && calleeName(node) === 'poll') {\n const fn = node.arguments[0];\n if (fn && isFunctionNode(fn)) callbacks.push(fn);\n }\n });\n return callbacks;\n}\n\nconst isRepeatingLoop = (\n node: ts.Node,\n): node is ts.ForStatement | ts.WhileStatement | ts.DoStatement =>\n ts.isForStatement(node) || ts.isWhileStatement(node) || ts.isDoStatement(node);\n\ntype FunctionBodies = ReadonlyMap<string, readonly ts.Node[]>;\n\nfunction functionBodiesOf(sf: ts.SourceFile): FunctionBodies {\n const bodies = new Map<string, ts.Node[]>();\n const add = (name: string, body: ts.Node) =>\n bodies.set(name, [...(bodies.get(name) ?? []), body]);\n forEachNode(sf, (node) => {\n if (ts.isFunctionDeclaration(node) && node.name && node.body) add(node.name.text, node.body);\n if (\n ts.isVariableDeclaration(node) &&\n ts.isIdentifier(node.name) &&\n node.initializer &&\n isFunctionNode(node.initializer)\n )\n add(node.name.text, node.initializer.body);\n });\n return bodies;\n}\n\nconst SOURCE_EXTENSIONS = ['.ts', '.tsx', '.mts', '.cts', '.js', '.jsx', '.mjs', '.cjs'];\n\nfunction moduleCandidates(base: string): string[] {\n const stem = base.replace(/\\.(?:[cm]?js|jsx)$/, '');\n return [\n base,\n ...SOURCE_EXTENSIONS.map((ext) => stem + ext),\n ...SOURCE_EXTENSIONS.map((ext) => `${base}/index${ext}`),\n ];\n}\n\n// Where an imported function lives: the file and the name it is declared under there.\nfunction namedImportsOf(\n file: FileView,\n paths: ReadonlySet<string>,\n): Map<string, { path: string; name: string }> {\n const imports = new Map<string, { path: string; name: string }>();\n for (const statement of file.sourceFile.statements) {\n if (!ts.isImportDeclaration(statement) || !ts.isStringLiteral(statement.moduleSpecifier))\n continue;\n const bindings = statement.importClause?.namedBindings;\n if (!bindings || !ts.isNamedImports(bindings)) continue;\n const base = resolveRelative(file.path, statement.moduleSpecifier.text)?.path;\n const target = base && moduleCandidates(base).find((candidate) => paths.has(candidate));\n if (!target) continue;\n for (const element of bindings.elements)\n imports.set(element.name.text, {\n path: target,\n name: (element.propertyName ?? element.name).text,\n });\n }\n return imports;\n}\n\n// Finds the first action a piece of code performs, directly or through the plain function calls it\n// makes into functions declared in the same file or imported from the suite's own files.\nexport interface ActionReach {\n actionIn(file: FileView, root: ts.Node): string[] | undefined;\n}\n\nexport function createActionReach(\n files: readonly FileView[],\n helpers: readonly string[] = [],\n): ActionReach {\n const actions = actionNamesOf(helpers);\n const byPath = new Map(files.map((file) => [file.path, file]));\n const paths = new Set(byPath.keys());\n const bodiesCache = new Map<string, FunctionBodies>();\n const importsCache = new Map<string, Map<string, { path: string; name: string }>>();\n const bodiesOf = (file: FileView) => {\n let bodies = bodiesCache.get(file.path);\n if (!bodies) bodiesCache.set(file.path, (bodies = functionBodiesOf(file.sourceFile)));\n return bodies;\n };\n const importsOf = (file: FileView) => {\n let imports = importsCache.get(file.path);\n if (!imports) importsCache.set(file.path, (imports = namedImportsOf(file, paths)));\n return imports;\n };\n\n function search(file: FileView, root: ts.Node, seen: Set<string>): string[] | undefined {\n let found: string[] | undefined;\n forEachNode(root, (node) => {\n if (found || !ts.isCallExpression(node)) return;\n const name = calleeName(node);\n if (!name) return;\n if (actions.has(name)) {\n found = [name];\n return;\n }\n if (!ts.isIdentifier(node.expression)) return;\n const imported = importsOf(file).get(name);\n const local = bodiesOf(file).get(name);\n const owner = local ? file : imported ? byPath.get(imported.path) : undefined;\n const declared = local ? name : imported?.name;\n if (!owner || !declared) return;\n const key = `${owner.path}#${declared}`;\n if (seen.has(key)) return;\n seen.add(key);\n for (const body of bodiesOf(owner).get(declared) ?? []) {\n const inner = search(owner, body, seen);\n if (inner) {\n found = [name, ...inner];\n return;\n }\n }\n });\n return found;\n }\n\n return { actionIn: (file, root) => search(file, root, new Set()) };\n}\n\n// `['openWork', 'goto']` reads \"goto() through openWork\".\nfunction describeAction(chain: readonly string[]): string {\n const action = `${chain[chain.length - 1]}()`;\n return chain.length > 1 ? `${action} through ${chain.slice(0, -1).join(' → ')}` : action;\n}\n\nconst within = (inner: ts.Node, outer: ts.Node): boolean =>\n inner !== outer && inner.pos >= outer.pos && inner.end <= outer.end;\n\nconst viewOf = (source: FileView | string) =>\n typeof source === 'string' ? viewOfText('sample.spec.ts', source) : source;\n\nexport function retriedActions(\n source: FileView | string,\n helpers: readonly string[] = [],\n reach?: ActionReach,\n): Occurrence[] {\n const file = viewOf(source);\n const sf = file.sourceFile;\n const reachFrom = reach ?? createActionReach([file], helpers);\n const loops: ts.Node[] = [];\n forEachNode(sf, (node) => {\n if (isRepeatingLoop(node)) loops.push(node);\n });\n const acting = [\n ...retriedCallbacks(sf).map((node) => ({\n node,\n body: node.body,\n kind: 'a retry loop',\n tail: ' until it passes',\n })),\n ...loops.map((node) => ({ node, body: node, kind: 'a for / while / do loop', tail: '' })),\n ].flatMap((loop) => {\n const chain = reachFrom.actionIn(file, loop.body);\n return chain ? [{ ...loop, chain }] : [];\n });\n return acting\n .filter((loop) => !acting.some((outer) => within(loop.node, outer.node)))\n .sort((a, b) => a.node.getStart(sf) - b.node.getStart(sf))\n .map((loop) => ({\n start: loop.node.getStart(sf),\n end: loop.node.getEnd(),\n detail: `${loop.kind} repeats ${describeAction(loop.chain)}${loop.tail}`,\n }));\n}\n\nfunction catchesRejection(handler: ts.Expression): boolean {\n if (!isFunctionNode(handler)) return true;\n let rethrows = false;\n forEachNode(handler.body, (node) => {\n if (ts.isThrowStatement(node)) rethrows = true;\n });\n return !rethrows;\n}\n\n// The call a `.catch()` is chained onto: `x.click().catch(…)` → the `click()` call.\nfunction caughtCall(catchCall: ts.CallExpression): ts.CallExpression | undefined {\n if (!ts.isPropertyAccessExpression(catchCall.expression)) return undefined;\n const receiver = catchCall.expression.expression;\n const call = ts.isParenthesizedExpression(receiver) ? receiver.expression : receiver;\n return ts.isCallExpression(call) ? call : undefined;\n}\n\nexport function swallowedActions(\n source: FileView | string,\n helpers: readonly string[] = [],\n reach?: ActionReach,\n): Occurrence[] {\n const file = viewOf(source);\n const sf = file.sourceFile;\n const reachFrom = reach ?? createActionReach([file], helpers);\n const hits: Occurrence[] = [];\n forEachNode(sf, (node) => {\n if (\n ts.isCallExpression(node) &&\n ts.isPropertyAccessExpression(node.expression) &&\n node.expression.name.text === 'catch' &&\n node.arguments.length === 1 &&\n catchesRejection(node.arguments[0])\n ) {\n const call = caughtCall(node);\n if (!call) return;\n const name = calleeName(call);\n const chain = reachFrom.actionIn(file, call);\n if (chain)\n hits.push({\n start: node.getStart(sf),\n end: node.getEnd(),\n detail: `a .catch() swallows a failed ${describeAction(chain)}`,\n });\n else if (name && STATE_PROBES.has(name))\n hits.push({\n start: node.getStart(sf),\n end: node.getEnd(),\n detail: `a .catch() turns a failed ${name}() — an ambiguous locator included — into a value`,\n });\n }\n if (ts.isTryStatement(node) && node.catchClause) {\n let rethrows = false;\n forEachNode(node.catchClause.block, (inner) => {\n if (ts.isThrowStatement(inner)) rethrows = true;\n });\n if (rethrows) return;\n const chain = reachFrom.actionIn(file, node.tryBlock);\n if (chain)\n hits.push({\n start: node.getStart(sf),\n end: node.getEnd(),\n detail: `a try/catch swallows a failed ${describeAction(chain)}`,\n });\n }\n });\n return hits;\n}\n\nfunction withReach(\n files: readonly FileView[],\n helpers: readonly string[],\n find: (file: FileView, reach: ActionReach) => Occurrence[],\n): Array<{ file: FileView; hits: Occurrence[] }> {\n const reach = createActionReach(files, helpers);\n return files.map((file) => ({ file, hits: find(file, reach) }));\n}\n\nexport interface E2eActionFacts {\n actionHelpers: readonly string[];\n reviewed: readonly AnchoredEntry[];\n}\n\nfunction e2eActionProblems(facts: E2eActionFacts): string[] {\n const problems: string[] = [];\n if (!objectProblem(facts, 'the facts', problems)) return problems;\n namesProblem(facts.actionHelpers, 'actionHelpers', problems, { nonEmpty: false });\n anchoredProblems(facts.reviewed, 'reviewed', problems);\n return problems;\n}\n\nexport const retryLoopActionGuard: GenericGuardDefinition<E2eActionFacts> = {\n id: 'retryLoopActions',\n absentWhen: 'the repository has no end-to-end suite',\n absentOnlyWithoutRoot: 'e2e',\n factProblems: e2eActionProblems,\n build(facts) {\n return [\n {\n title: 'no end-to-end retry loop repeats a user action',\n description:\n 'A toPass() or expect.poll() callback, or a for (;;) / while / do loop, that clicks, types, or navigates ' +\n 'repeats the action until it happens to work, so the failure a real user would hit passes as green ' +\n '(RULE-STD-QUALITY-003). Walking a list is written for…of, which is not a repeat. Act once, ' +\n 'then assert with a web-first assertion; a loop that only re-reads state is fine. The actions are ' +\n \"Playwright's input and navigation calls plus the app's declared actionHelpers, reached directly or through \" +\n 'a plain call to a function the suite declares in the same file or imports from its own files. A loop that must act — ' +\n 'polling a background result by reloading — is a reviewed entry whose anchor names that one loop, with ' +\n 'its why; an entry that no longer matches a loop is stale.',\n timeoutMs: REPOSITORY_GUARD_TIMEOUT_MS,\n run: (context) =>\n anchoredFindings(\n withReach(context.files(E2E_SCOPE), facts.actionHelpers, (file, reach) =>\n retriedActions(file, facts.actionHelpers, reach),\n ),\n facts.reviewed,\n 'its anchor names no retry loop that acts',\n ),\n },\n ];\n },\n};\n\nexport const swallowedActionGuard: GenericGuardDefinition<E2eActionFacts> = {\n id: 'swallowedE2eActions',\n absentWhen: 'the repository has no end-to-end suite',\n absentOnlyWithoutRoot: 'e2e',\n factProblems: e2eActionProblems,\n build(facts) {\n return [\n {\n title: 'no end-to-end action or state read swallows its failure',\n description:\n 'A .catch() or a try/catch that discards a failed click, fill, or navigation — made directly or through a plain ' +\n \"call to one of the suite's own functions — lets the next step run on a \" +\n 'page the action never reached, and one on isVisible(), isEnabled(), or isChecked() turns an ambiguous ' +\n '(strict-mode) locator into a silent \"no\". Let the failure throw: an action whose outcome is uncertain ' +\n \"goes through the suite's outcome-verified helper, and a branch reads state with no catch. A catch that \" +\n 'must stay is a reviewed entry whose anchor names that one occurrence, with its why; an entry that no ' +\n 'longer matches one is stale.',\n timeoutMs: REPOSITORY_GUARD_TIMEOUT_MS,\n run: (context) =>\n anchoredFindings(\n withReach(context.files(E2E_SCOPE), facts.actionHelpers, (file, reach) =>\n swallowedActions(file, facts.actionHelpers, reach),\n ),\n facts.reviewed,\n 'its anchor names no swallowed action or state read',\n ),\n },\n ];\n },\n};\n"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"vitest-report.d.ts","sourceRoot":"","sources":["../../../src/node/tooling/vitest-report.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,eAAe;IAC9B,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,eAAe,CAAC,EAAE,MAAM,EAAE,CAAC;CAC5B;AAED,MAAM,WAAW,gBAAgB;IAC/B,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,gBAAgB,CAAC,EAAE,eAAe,EAAE,CAAC;CACtC;AAED,MAAM,WAAW,gBAAgB;IAC/B,WAAW,CAAC,EAAE,gBAAgB,EAAE,CAAC;IACjC,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AAED,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,EAAE,CAAC;CACpB;AAED,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,YAAY;IAC3B,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;IACd,YAAY,EAAE,MAAM,CAAC;CACtB;AAGD,wBAAgB,eAAe,CAAC,MAAM,EAAE,MAAM,GAAG,gBAAgB,GAAG,IAAI,CAcvE;
|
|
1
|
+
{"version":3,"file":"vitest-report.d.ts","sourceRoot":"","sources":["../../../src/node/tooling/vitest-report.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,eAAe;IAC9B,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,eAAe,CAAC,EAAE,MAAM,EAAE,CAAC;CAC5B;AAED,MAAM,WAAW,gBAAgB;IAC/B,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,gBAAgB,CAAC,EAAE,eAAe,EAAE,CAAC;CACtC;AAED,MAAM,WAAW,gBAAgB;IAC/B,WAAW,CAAC,EAAE,gBAAgB,EAAE,CAAC;IACjC,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AAED,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,EAAE,CAAC;CACpB;AAED,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,YAAY;IAC3B,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;IACd,YAAY,EAAE,MAAM,CAAC;CACtB;AAGD,wBAAgB,eAAe,CAAC,MAAM,EAAE,MAAM,GAAG,gBAAgB,GAAG,IAAI,CAcvE;AAcD,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,gBAAgB,GAAG,IAAI,GAAG;IACtE,QAAQ,EAAE,WAAW,EAAE,CAAC;IACxB,aAAa,EAAE,YAAY,EAAE,CAAC;IAC9B,YAAY,EAAE,WAAW,EAAE,CAAC;IAC5B,MAAM,EAAE,YAAY,GAAG,IAAI,CAAC;CAC7B,CAoCA;AAED,wBAAgB,kBAAkB,CAAC,EACjC,IAAI,EACJ,MAAM,EACN,aAAa,GACd,EAAE;IACD,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,YAAY,GAAG,IAAI,CAAC;IAC5B,aAAa,EAAE,YAAY,EAAE,CAAC;CAC/B,GAAG;IAAE,EAAE,EAAE,OAAO,CAAC;IAAC,aAAa,EAAE,MAAM,GAAG,IAAI,CAAA;CAAE,CAWhD;AAED,wBAAgB,mBAAmB,CAAC,aAAa,EAAE,YAAY,EAAE,GAAG,MAAM,CAUzE"}
|
|
@@ -17,6 +17,15 @@ export function parseVitestJson(stdout) {
|
|
|
17
17
|
}
|
|
18
18
|
}
|
|
19
19
|
}
|
|
20
|
+
const TIMEOUT_PLACEHOLDER = 'Error: STACK_TRACE_ERROR';
|
|
21
|
+
// Vitest reports a test that passed its time limit with the stack of a placeholder error, so the
|
|
22
|
+
// report carries `STACK_TRACE_ERROR` where the "timed out" message should be.
|
|
23
|
+
function readableFailureMessage(message, durationMs) {
|
|
24
|
+
if (!message.startsWith(TIMEOUT_PLACEHOLDER))
|
|
25
|
+
return message;
|
|
26
|
+
const ran = typeof durationMs === 'number' ? ` after ${Math.round(durationMs)} ms` : '';
|
|
27
|
+
return `Timed out${ran}: the test passed its time limit.${message.slice(TIMEOUT_PLACEHOLDER.length)}`;
|
|
28
|
+
}
|
|
20
29
|
// A test file that failed to load carries its error as the file's own `message` and adds nothing
|
|
21
30
|
// to `numFailedTests`.
|
|
22
31
|
export function collectVitestFailures(report) {
|
|
@@ -31,7 +40,11 @@ export function collectVitestFailures(report) {
|
|
|
31
40
|
const title = a.fullName || a.title || '';
|
|
32
41
|
if (a.status === 'failed') {
|
|
33
42
|
failedTests += 1;
|
|
34
|
-
failures.push({
|
|
43
|
+
failures.push({
|
|
44
|
+
file: file.name,
|
|
45
|
+
title,
|
|
46
|
+
messages: (a.failureMessages ?? []).map((m) => readableFailureMessage(m, a.duration)),
|
|
47
|
+
});
|
|
35
48
|
}
|
|
36
49
|
else if (a.status === 'skipped' || a.status === 'pending' || a.status === 'todo') {
|
|
37
50
|
skippedTests.push({ file: file.name, title, status: a.status });
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"vitest-report.js","sourceRoot":"","sources":["../../../src/node/tooling/vitest-report.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"vitest-report.js","sourceRoot":"","sources":["../../../src/node/tooling/vitest-report.ts"],"names":[],"mappings":"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","sourcesContent":["export interface VitestAssertion {\n status?: string;\n title?: string;\n fullName?: string;\n duration?: number | null;\n failureMessages?: string[];\n}\n\nexport interface VitestFileResult {\n name: string;\n status?: string;\n message?: string;\n assertionResults?: VitestAssertion[];\n}\n\nexport interface VitestJsonReport {\n testResults?: VitestFileResult[];\n numPassedTests?: number;\n numFailedTests?: number;\n numPendingTests?: number;\n numTodoTests?: number;\n numTotalTests?: number;\n}\n\nexport interface TestFailure {\n file: string;\n title: string;\n messages: string[];\n}\n\nexport interface SuiteFailure {\n file: string;\n message: string;\n}\n\nexport interface SkippedTest {\n file: string;\n title: string;\n status: string;\n}\n\nexport interface VitestTotals {\n passed: number;\n failed: number;\n skipped: number;\n total: number;\n failedSuites: number;\n}\n\n// Vitest may print other text around the JSON object.\nexport function parseVitestJson(stdout: string): VitestJsonReport | null {\n const trimmed = stdout.trim();\n try {\n return JSON.parse(trimmed) as VitestJsonReport;\n } catch {\n const first = trimmed.indexOf('{');\n const last = trimmed.lastIndexOf('}');\n if (first === -1 || last === -1 || last < first) return null;\n try {\n return JSON.parse(trimmed.slice(first, last + 1)) as VitestJsonReport;\n } catch {\n return null;\n }\n }\n}\n\nconst TIMEOUT_PLACEHOLDER = 'Error: STACK_TRACE_ERROR';\n\n// Vitest reports a test that passed its time limit with the stack of a placeholder error, so the\n// report carries `STACK_TRACE_ERROR` where the \"timed out\" message should be.\nfunction readableFailureMessage(message: string, durationMs?: number | null): string {\n if (!message.startsWith(TIMEOUT_PLACEHOLDER)) return message;\n const ran = typeof durationMs === 'number' ? ` after ${Math.round(durationMs)} ms` : '';\n return `Timed out${ran}: the test passed its time limit.${message.slice(TIMEOUT_PLACEHOLDER.length)}`;\n}\n\n// A test file that failed to load carries its error as the file's own `message` and adds nothing\n// to `numFailedTests`.\nexport function collectVitestFailures(report: VitestJsonReport | null): {\n failures: TestFailure[];\n suiteFailures: SuiteFailure[];\n skippedTests: SkippedTest[];\n totals: VitestTotals | null;\n} {\n if (!report) return { failures: [], suiteFailures: [], skippedTests: [], totals: null };\n const failures: TestFailure[] = [];\n const suiteFailures: SuiteFailure[] = [];\n const skippedTests: SkippedTest[] = [];\n for (const file of report.testResults ?? []) {\n let failedTests = 0;\n for (const a of file.assertionResults ?? []) {\n const title = a.fullName || a.title || '';\n if (a.status === 'failed') {\n failedTests += 1;\n failures.push({\n file: file.name,\n title,\n messages: (a.failureMessages ?? []).map((m) => readableFailureMessage(m, a.duration)),\n });\n } else if (a.status === 'skipped' || a.status === 'pending' || a.status === 'todo') {\n skippedTests.push({ file: file.name, title, status: a.status });\n }\n }\n if (file.status === 'failed' && failedTests === 0) {\n suiteFailures.push({\n file: file.name,\n message: file.message || '(no error message in the report)',\n });\n }\n }\n const totals: VitestTotals = {\n passed: report.numPassedTests ?? 0,\n failed: report.numFailedTests ?? 0,\n // `.skip` and `xit` land in numPendingTests; `.todo` is counted on its own.\n skipped: (report.numPendingTests ?? 0) + (report.numTodoTests ?? 0),\n total: report.numTotalTests ?? 0,\n failedSuites: suiteFailures.length,\n };\n return { failures, suiteFailures, skippedTests, totals };\n}\n\nexport function vitestStageVerdict({\n code,\n totals,\n suiteFailures,\n}: {\n code: number;\n totals: VitestTotals | null;\n suiteFailures: SuiteFailure[];\n}): { ok: boolean; skippedReason: string | null } {\n const skipped = totals?.skipped ?? 0;\n const ok =\n code === 0 &&\n totals !== null &&\n totals.failed === 0 &&\n skipped === 0 &&\n suiteFailures.length === 0;\n const skippedReason =\n skipped > 0 ? `${skipped} skipped/pending test(s) make this stage non-green.` : null;\n return { ok, skippedReason };\n}\n\nexport function formatSuiteFailures(suiteFailures: SuiteFailure[]): string {\n return suiteFailures\n .map(\n (suite) =>\n `Failed to load: ${suite.file}\\n${suite.message\n .split('\\n')\n .map((line) => ` ${line}`)\n .join('\\n')}`,\n )\n .join('\\n\\n');\n}\n"]}
|