@ttt-productions/repo-guards 0.1.22 → 0.1.24

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.
@@ -27,7 +27,7 @@ export interface TextFieldDeclarationFacts {
27
27
  why: string;
28
28
  }>;
29
29
  declarations: Loader<Readonly<Record<string, unknown>>>;
30
- fieldSchema: Loader<FieldSchemaOf['schemaOf']>;
30
+ fieldSchema: Loader<FieldSchemaOf>;
31
31
  }
32
32
  interface Site {
33
33
  key: string;
@@ -1 +1 @@
1
- {"version":3,"file":"text-field-declarations.d.ts","sourceRoot":"","sources":["../../../src/modules/core-definitions/text-field-declarations.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,4BAA4B,CAAC;AACzE,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,sBAAsB,CAAC;AAErD,OAAO,EAIL,KAAK,MAAM,EAEZ,MAAM,wBAAwB,CAAC;AAIhC,KAAK,cAAc,GAAG,aAAa,CAAC;IAClC,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,KAAK,EAAE,MAAM,CAAC;IACd,GAAG,EAAE,MAAM,CAAC;CACb,CAAC,CAAC;AACH,KAAK,WAAW,GAAG;IAAE,SAAS,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK;QAAE,OAAO,EAAE,OAAO,CAAC;QAAC,IAAI,CAAC,EAAE,OAAO,CAAA;KAAE,CAAA;CAAE,CAAC;AAI3F,UAAU,aAAa;IACrB,QAAQ,CAAC,WAAW,EAAE,OAAO,GAAG,WAAW,CAAC;CAC7C;AAED,MAAM,WAAW,yBAAyB;IACxC,WAAW,EAAE,SAAS,MAAM,EAAE,CAAC;IAC/B,gBAAgB,EAAE,MAAM,CAAC;IACzB,qBAAqB,EAAE,SAAS,MAAM,EAAE,CAAC;IACzC,WAAW,EAAE,cAAc,CAAC;IAC5B,kBAAkB,EAAE,cAAc,CAAC;IACnC,WAAW,EAAE,aAAa,CAAC;QAAE,WAAW,EAAE,MAAM,CAAC;QAAC,GAAG,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IACjE,YAAY,EAAE,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC,CAAC;IACxD,WAAW,EAAE,MAAM,CAAC,aAAa,CAAC,UAAU,CAAC,CAAC,CAAC;CAChD;AA+BD,UAAU,IAAI;IACZ,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;CACd;AAED,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,QAAQ,GAAG,IAAI,EAAE,CAyBxD;AAED,wBAAgB,kBAAkB,CAChC,IAAI,EAAE,QAAQ,EACd,WAAW,EAAE,CAAC,WAAW,EAAE,MAAM,KAAK,OAAO,GAC5C,IAAI,EAAE,CAiCR;AAsDD,eAAO,MAAM,0BAA0B,EAAE,sBAAsB,CAAC,yBAAyB,CAmKxF,CAAC"}
1
+ {"version":3,"file":"text-field-declarations.d.ts","sourceRoot":"","sources":["../../../src/modules/core-definitions/text-field-declarations.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,4BAA4B,CAAC;AACzE,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,sBAAsB,CAAC;AAErD,OAAO,EAIL,KAAK,MAAM,EAEZ,MAAM,wBAAwB,CAAC;AAIhC,KAAK,cAAc,GAAG,aAAa,CAAC;IAClC,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,KAAK,EAAE,MAAM,CAAC;IACd,GAAG,EAAE,MAAM,CAAC;CACb,CAAC,CAAC;AACH,KAAK,WAAW,GAAG;IAAE,SAAS,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK;QAAE,OAAO,EAAE,OAAO,CAAC;QAAC,IAAI,CAAC,EAAE,OAAO,CAAA;KAAE,CAAA;CAAE,CAAC;AAI3F,UAAU,aAAa;IACrB,QAAQ,CAAC,WAAW,EAAE,OAAO,GAAG,WAAW,CAAC;CAC7C;AAED,MAAM,WAAW,yBAAyB;IACxC,WAAW,EAAE,SAAS,MAAM,EAAE,CAAC;IAC/B,gBAAgB,EAAE,MAAM,CAAC;IACzB,qBAAqB,EAAE,SAAS,MAAM,EAAE,CAAC;IACzC,WAAW,EAAE,cAAc,CAAC;IAC5B,kBAAkB,EAAE,cAAc,CAAC;IACnC,WAAW,EAAE,aAAa,CAAC;QAAE,WAAW,EAAE,MAAM,CAAC;QAAC,GAAG,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IACjE,YAAY,EAAE,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC,CAAC;IACxD,WAAW,EAAE,MAAM,CAAC,aAAa,CAAC,CAAC;CACpC;AA+BD,UAAU,IAAI;IACZ,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;CACd;AAED,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,QAAQ,GAAG,IAAI,EAAE,CAyBxD;AAED,wBAAgB,kBAAkB,CAChC,IAAI,EAAE,QAAQ,EACd,WAAW,EAAE,CAAC,WAAW,EAAE,MAAM,KAAK,OAAO,GAC5C,IAAI,EAAE,CAiCR;AAsDD,eAAO,MAAM,0BAA0B,EAAE,sBAAsB,CAAC,yBAAyB,CAmKxF,CAAC"}
@@ -232,14 +232,14 @@ export const textFieldDeclarationsGuard = {
232
232
  const findings = [];
233
233
  coreBuild(context, findings);
234
234
  const declarations = textDeclarations(await facts.declarations());
235
- const schemaOf = await facts.fieldSchema();
235
+ const reader = await facts.fieldSchema();
236
236
  if (!declarations.length)
237
237
  findings.push({
238
238
  file: facts.declarationsFile,
239
239
  detail: 'no text-field declaration was loaded',
240
240
  });
241
241
  for (const [name, declaration] of declarations) {
242
- const schema = schemaOf(declaration);
242
+ const schema = reader.schemaOf(declaration);
243
243
  const min = typeof declaration.min === 'number' ? declaration.min : 0;
244
244
  if (min >= 1 && schema.safeParse(' \n\t ').success)
245
245
  findings.push({
@@ -1 +1 @@
1
- {"version":3,"file":"text-field-declarations.js","sourceRoot":"","sources":["../../../src/modules/core-definitions/text-field-declarations.ts"],"names":[],"mappings":"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","sourcesContent":["import ts from 'typescript';\nimport type { Finding, GuardContext } from '../../guard.js';\nimport type { GenericGuardDefinition } from '../../node/suite/config.js';\nimport type { FileView } from '../../reader/view.js';\nimport { lineOfNode } from '../../support/ast.js';\nimport {\n namesProblem,\n reviewedProblems,\n loaderProblem,\n type Loader,\n textProblem,\n} from '../../support/facts.js';\nimport { CORE_CODE, coreBuild, filesIn, isCount, topLevelDeclaration } from './shared.js';\nimport { REPOSITORY_GUARD_TIMEOUT_MS } from '../../support/timeouts.js';\n\ntype ReviewedCounts = ReadonlyArray<{\n file: string;\n declaration: string;\n count: number;\n why: string;\n}>;\ntype FieldSchema = { safeParse: (value: unknown) => { success: boolean; data?: unknown } };\n\n// A method, so the app's own reader — typed on its declaration type — fits it as declared, and the\n// guard hands it the declarations it loaded from that same module.\ninterface FieldSchemaOf {\n schemaOf(declaration: unknown): FieldSchema;\n}\n\nexport interface TextFieldDeclarationFacts {\n wireFolders: readonly string[];\n declarationsFile: string;\n wireInputNameSuffixes: readonly string[];\n selfBounded: ReviewedCounts;\n undeclaredFreeText: ReviewedCounts;\n keepAsTyped: ReadonlyArray<{ declaration: string; why: string }>;\n declarations: Loader<Readonly<Record<string, unknown>>>;\n fieldSchema: Loader<FieldSchemaOf['schemaOf']>;\n}\n\nconst isZString = (node: ts.Node): node is ts.CallExpression =>\n ts.isCallExpression(node) &&\n ts.isPropertyAccessExpression(node.expression) &&\n node.expression.name.text === 'string' &&\n ts.isIdentifier(node.expression.expression) &&\n node.expression.expression.text === 'z';\n\nconst TEXT_LENGTH_CONSTANT = /^(MAX|MIN)_(?![A-Z0-9_]*_ID_LENGTH$)[A-Z0-9_]*LENGTH$/;\nconst FREE_TEXT_PROPERTY =\n /reason|message|text|note|comment|title|description|name|subject|caption|summary|statement|facts|letter|label|heading|body|query|content|suggestion|region|country|value|intro|points|strings|term/i;\nconst BUILT_IN_FORMAT_METHODS = new Set(['regex', 'url', 'email', 'uuid']);\n\nfunction chainedCalls(root: ts.CallExpression): ts.CallExpression[] {\n const calls: ts.CallExpression[] = [];\n let node: ts.Node = root;\n while (\n ts.isPropertyAccessExpression(node.parent) &&\n node.parent.expression === node &&\n ts.isCallExpression(node.parent.parent)\n ) {\n calls.push(node.parent.parent);\n node = node.parent.parent;\n }\n return calls;\n}\n\nconst methodOf = (call: ts.CallExpression) =>\n (call.expression as ts.PropertyAccessExpression).name.text;\n\ninterface Site {\n key: string;\n line: number;\n}\n\nexport function selfBoundedChains(file: FileView): Site[] {\n const sites: Site[] = [];\n const visit = (node: ts.Node): void => {\n if (isZString(node)) {\n const bounded = chainedCalls(node).some((call) => {\n const method = methodOf(call);\n if (method === 'trim') return true;\n if (method !== 'min' && method !== 'max') return false;\n const [argument] = call.arguments;\n return (\n argument !== undefined &&\n ts.isIdentifier(argument) &&\n TEXT_LENGTH_CONSTANT.test(argument.text)\n );\n });\n if (bounded)\n sites.push({\n key: `${file.path}#${topLevelDeclaration(node)}`,\n line: lineOfNode(file, node),\n });\n }\n ts.forEachChild(node, visit);\n };\n visit(file.sourceFile);\n return sites;\n}\n\nexport function undeclaredFreeText(\n file: FileView,\n isWireInput: (declaration: string) => boolean,\n): Site[] {\n const sites: Site[] = [];\n const visit = (node: ts.Node): void => {\n if (isZString(node)) {\n const declaration = topLevelDeclaration(node);\n const calls = chainedCalls(node);\n const outer: ts.Node = calls.length ? calls[calls.length - 1] : node;\n const parent = outer.parent;\n let property: string | null = null;\n if (parent && ts.isPropertyAssignment(parent))\n property = parent.name.getText(file.sourceFile);\n else if (\n parent &&\n ts.isCallExpression(parent) &&\n parent.parent &&\n ts.isPropertyAssignment(parent.parent)\n ) {\n property = parent.parent.name.getText(file.sourceFile);\n }\n const builtIn = calls.some((call) => BUILT_IN_FORMAT_METHODS.has(methodOf(call)));\n if (\n property !== null &&\n isWireInput(declaration) &&\n FREE_TEXT_PROPERTY.test(property) &&\n !builtIn\n ) {\n sites.push({ key: `${file.path}#${declaration}`, line: lineOfNode(file, node) });\n }\n }\n ts.forEachChild(node, visit);\n };\n visit(file.sourceFile);\n return sites;\n}\n\nfunction compareReviewed(\n sites: readonly Site[],\n reviewed: ReviewedCounts,\n what: string,\n): Finding[] {\n const findings: Finding[] = [];\n const byKey = new Map<string, Site[]>();\n for (const site of sites) byKey.set(site.key, [...(byKey.get(site.key) ?? []), site]);\n const allowed = new Map(reviewed.map((e) => [`${e.file}#${e.declaration}`, e.count]));\n for (const [key, found] of byKey) {\n const count = allowed.get(key) ?? 0;\n const file = key.slice(0, key.indexOf('#'));\n for (const site of found.slice(count))\n findings.push({\n file,\n line: site.line,\n detail: `${what} in ${key.slice(key.indexOf('#') + 1)}${count ? ` beyond the ${count} reviewed` : ''}`,\n });\n }\n for (const [key, count] of allowed) {\n const found = byKey.get(key)?.length ?? 0;\n if (found < count)\n findings.push({\n file: key.slice(0, key.indexOf('#')),\n detail: `stale reviewed count for ${key}: reviewed ${count}, found ${found}`,\n });\n }\n return findings;\n}\n\ntype Declaration = { format?: unknown; min?: number; keepAsTyped?: unknown };\n\nconst FREE_TEXT_FORMAT = 'none';\nconst textDeclarations = (exports: Readonly<Record<string, unknown>>) =>\n Object.entries(exports).filter(\n (e): e is [string, Declaration] =>\n typeof e[1] === 'object' && e[1] !== null && 'format' in e[1],\n );\n\nconst reviewedCountProblems = (value: unknown, what: string, problems: string[]) =>\n reviewedProblems(value, what, problems, {\n key: 'file',\n id: (e) =>\n typeof e.declaration === 'string' && e.declaration.trim()\n ? `${e.file}#${e.declaration}`\n : undefined,\n idNames: 'declaration',\n extra(entry, label) {\n if (!isCount(entry.count)) problems.push(`${label}'s count must be a positive whole number`);\n },\n });\n\nexport const textFieldDeclarationsGuard: GenericGuardDefinition<TextFieldDeclarationFacts> = {\n id: 'textFieldDeclarations',\n absentWhen: \"the app's core declares no text field through input-format-core\",\n factProblems(facts) {\n const problems: string[] = [];\n namesProblem(facts?.wireFolders, 'wireFolders', problems);\n textProblem(facts?.declarationsFile, 'declarationsFile', problems);\n namesProblem(facts?.wireInputNameSuffixes, 'wireInputNameSuffixes', problems);\n reviewedCountProblems(facts?.selfBounded, 'selfBounded', problems);\n reviewedCountProblems(facts?.undeclaredFreeText, 'undeclaredFreeText', problems);\n reviewedProblems(facts?.keepAsTyped, 'keepAsTyped', problems, { key: 'declaration' });\n loaderProblem(facts?.declarations, 'declarations', problems);\n loaderProblem(facts?.fieldSchema, 'fieldSchema', problems);\n return problems;\n },\n build(facts) {\n const isWireInput = (name: string) => facts.wireInputNameSuffixes.some((s) => name.endsWith(s));\n const wireFiles = (context: GuardContext, findings: Finding[]) =>\n filesIn(context.files(CORE_CODE), facts.wireFolders, findings);\n return [\n {\n title: 'every free-text property of a core wire input is a declared text field',\n description:\n 'A wire input property that names free text (reason, message, title, …) reads its declared text field through ' +\n \"the core's field-schema builder, or carries a built-in format (.regex / .url / .email / .uuid). Wire text no \" +\n 'person types is a reviewed module#declaration count with why; a count that no longer matches fails.',\n timeoutMs: REPOSITORY_GUARD_TIMEOUT_MS,\n run(context) {\n const findings: Finding[] = [];\n const sites = wireFiles(context, findings).flatMap((f) =>\n undeclaredFreeText(f, isWireInput),\n );\n return [\n ...findings,\n ...compareReviewed(\n sites,\n facts.undeclaredFreeText,\n 'an undeclared free-text z.string() property',\n ),\n ];\n },\n },\n {\n title: 'no core wire schema trims or bounds a text field itself',\n description:\n 'A wire z.string() chain never trims itself or bounds itself by a text-length constant: that is a second ' +\n \"declaration of the field. Read the field's declaration through the core's field-schema builder. Wire text no \" +\n 'person types (stored values, fixed strings, ids, addresses) is a reviewed module#declaration count with why.',\n timeoutMs: REPOSITORY_GUARD_TIMEOUT_MS,\n run(context) {\n const findings: Finding[] = [];\n const sites = wireFiles(context, findings).flatMap(selfBoundedChains);\n return [\n ...findings,\n ...compareReviewed(sites, facts.selfBounded, 'a self-bounded z.string() chain'),\n ];\n },\n },\n {\n title: 'text fields are declared only in the core declarations module',\n description:\n 'defineInputFormat is called, and keepAsTyped is set, only in the one module that declares every text field, ' +\n 'so each field has one declaration. Move the declaration there and import it.',\n timeoutMs: REPOSITORY_GUARD_TIMEOUT_MS,\n run(context) {\n const findings: Finding[] = [];\n const files = context.files(CORE_CODE);\n if (!files.some((f) => f.path === facts.declarationsFile)) {\n findings.push({\n file: facts.declarationsFile,\n detail: 'the declarations module is not in the core build',\n });\n }\n for (const file of files) {\n if (file.path === facts.declarationsFile) continue;\n for (const m of file.code.matchAll(/\\bdefineInputFormat\\s*\\(|\\bkeepAsTyped\\b/g)) {\n findings.push({\n file: file.path,\n line: file.lineOf(m.index ?? 0),\n detail: `${m[0].replace(/\\s*\\($/, '')} outside the declarations module`,\n });\n }\n }\n return findings;\n },\n },\n {\n title: 'only the reviewed text fields keep their text as typed',\n description:\n 'Every declared text field is trimmed unless it is a reviewed keep-as-typed declaration with why (a legal or ' +\n 'statutory text whose exact characters matter). A declaration that newly keeps its text fails until reviewed, ' +\n 'and a reviewed one that no longer does is stale.',\n async run(context) {\n const findings: Finding[] = [];\n coreBuild(context, findings);\n const kept = new Set(\n textDeclarations(await facts.declarations())\n .filter(([, d]) => 'keepAsTyped' in d)\n .map(([n]) => n),\n );\n const reviewed = new Set(facts.keepAsTyped.map((e) => e.declaration));\n for (const name of [...kept].sort())\n if (!reviewed.has(name))\n findings.push({\n file: `(core) ${name}`,\n detail: 'keeps its text as typed without review',\n });\n for (const name of reviewed)\n if (!kept.has(name))\n findings.push({\n file: `(core) ${name}`,\n detail:\n 'stale keep-as-typed entry: the declaration no longer keeps its text, or is gone',\n });\n return findings;\n },\n },\n {\n title:\n 'every declared text field trims, one that holds words keeps one space for each run of spaces, and a required one refuses blank text',\n description:\n \"Each text field's wire schema, built from its declaration, refuses whitespace-only text when the field requires \" +\n 'text (min of 1 or more), and trims the text of every field that does not keep it as typed; a field whose format ' +\n \"holds words (every format but free text, 'none') keeps one space for a run of them — so the form, the wire, and the writer agree, and a \" +\n 'name spaced twice is never stored, searched, or reserved apart from the same name spaced once.',\n async run(context) {\n const findings: Finding[] = [];\n coreBuild(context, findings);\n const declarations = textDeclarations(await facts.declarations());\n const schemaOf = await facts.fieldSchema();\n if (!declarations.length)\n findings.push({\n file: facts.declarationsFile,\n detail: 'no text-field declaration was loaded',\n });\n for (const [name, declaration] of declarations) {\n const schema = schemaOf(declaration);\n const min = typeof declaration.min === 'number' ? declaration.min : 0;\n if (min >= 1 && schema.safeParse(' \\n\\t ').success)\n findings.push({\n file: `(core) ${name}`,\n detail: 'accepts whitespace-only text although the field requires text',\n });\n if ('keepAsTyped' in declaration) continue;\n const sample = 'a'.repeat(Math.max(min, 1));\n const parsed = schema.safeParse(` ${sample} `);\n if (!parsed.success || parsed.data !== sample)\n findings.push({ file: `(core) ${name}`, detail: 'does not trim its text' });\n if (declaration.format === FREE_TEXT_FORMAT) continue;\n const word = 'a'.repeat(Math.max(min - 2, 1));\n if (!schema.safeParse(`${word} a`).success) continue;\n const run = schema.safeParse(`${word} a`);\n if (!run.success || run.data !== `${word} a`)\n findings.push({\n file: `(core) ${name}`,\n detail: 'keeps a run of spaces instead of one space',\n });\n }\n return findings;\n },\n },\n ];\n },\n};\n"]}
1
+ {"version":3,"file":"text-field-declarations.js","sourceRoot":"","sources":["../../../src/modules/core-definitions/text-field-declarations.ts"],"names":[],"mappings":"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","sourcesContent":["import ts from 'typescript';\nimport type { Finding, GuardContext } from '../../guard.js';\nimport type { GenericGuardDefinition } from '../../node/suite/config.js';\nimport type { FileView } from '../../reader/view.js';\nimport { lineOfNode } from '../../support/ast.js';\nimport {\n namesProblem,\n reviewedProblems,\n loaderProblem,\n type Loader,\n textProblem,\n} from '../../support/facts.js';\nimport { CORE_CODE, coreBuild, filesIn, isCount, topLevelDeclaration } from './shared.js';\nimport { REPOSITORY_GUARD_TIMEOUT_MS } from '../../support/timeouts.js';\n\ntype ReviewedCounts = ReadonlyArray<{\n file: string;\n declaration: string;\n count: number;\n why: string;\n}>;\ntype FieldSchema = { safeParse: (value: unknown) => { success: boolean; data?: unknown } };\n\n// A method, so the app's own reader — typed on its declaration type — fits it as declared, and the\n// guard hands it the declarations it loaded from that same module.\ninterface FieldSchemaOf {\n schemaOf(declaration: unknown): FieldSchema;\n}\n\nexport interface TextFieldDeclarationFacts {\n wireFolders: readonly string[];\n declarationsFile: string;\n wireInputNameSuffixes: readonly string[];\n selfBounded: ReviewedCounts;\n undeclaredFreeText: ReviewedCounts;\n keepAsTyped: ReadonlyArray<{ declaration: string; why: string }>;\n declarations: Loader<Readonly<Record<string, unknown>>>;\n fieldSchema: Loader<FieldSchemaOf>;\n}\n\nconst isZString = (node: ts.Node): node is ts.CallExpression =>\n ts.isCallExpression(node) &&\n ts.isPropertyAccessExpression(node.expression) &&\n node.expression.name.text === 'string' &&\n ts.isIdentifier(node.expression.expression) &&\n node.expression.expression.text === 'z';\n\nconst TEXT_LENGTH_CONSTANT = /^(MAX|MIN)_(?![A-Z0-9_]*_ID_LENGTH$)[A-Z0-9_]*LENGTH$/;\nconst FREE_TEXT_PROPERTY =\n /reason|message|text|note|comment|title|description|name|subject|caption|summary|statement|facts|letter|label|heading|body|query|content|suggestion|region|country|value|intro|points|strings|term/i;\nconst BUILT_IN_FORMAT_METHODS = new Set(['regex', 'url', 'email', 'uuid']);\n\nfunction chainedCalls(root: ts.CallExpression): ts.CallExpression[] {\n const calls: ts.CallExpression[] = [];\n let node: ts.Node = root;\n while (\n ts.isPropertyAccessExpression(node.parent) &&\n node.parent.expression === node &&\n ts.isCallExpression(node.parent.parent)\n ) {\n calls.push(node.parent.parent);\n node = node.parent.parent;\n }\n return calls;\n}\n\nconst methodOf = (call: ts.CallExpression) =>\n (call.expression as ts.PropertyAccessExpression).name.text;\n\ninterface Site {\n key: string;\n line: number;\n}\n\nexport function selfBoundedChains(file: FileView): Site[] {\n const sites: Site[] = [];\n const visit = (node: ts.Node): void => {\n if (isZString(node)) {\n const bounded = chainedCalls(node).some((call) => {\n const method = methodOf(call);\n if (method === 'trim') return true;\n if (method !== 'min' && method !== 'max') return false;\n const [argument] = call.arguments;\n return (\n argument !== undefined &&\n ts.isIdentifier(argument) &&\n TEXT_LENGTH_CONSTANT.test(argument.text)\n );\n });\n if (bounded)\n sites.push({\n key: `${file.path}#${topLevelDeclaration(node)}`,\n line: lineOfNode(file, node),\n });\n }\n ts.forEachChild(node, visit);\n };\n visit(file.sourceFile);\n return sites;\n}\n\nexport function undeclaredFreeText(\n file: FileView,\n isWireInput: (declaration: string) => boolean,\n): Site[] {\n const sites: Site[] = [];\n const visit = (node: ts.Node): void => {\n if (isZString(node)) {\n const declaration = topLevelDeclaration(node);\n const calls = chainedCalls(node);\n const outer: ts.Node = calls.length ? calls[calls.length - 1] : node;\n const parent = outer.parent;\n let property: string | null = null;\n if (parent && ts.isPropertyAssignment(parent))\n property = parent.name.getText(file.sourceFile);\n else if (\n parent &&\n ts.isCallExpression(parent) &&\n parent.parent &&\n ts.isPropertyAssignment(parent.parent)\n ) {\n property = parent.parent.name.getText(file.sourceFile);\n }\n const builtIn = calls.some((call) => BUILT_IN_FORMAT_METHODS.has(methodOf(call)));\n if (\n property !== null &&\n isWireInput(declaration) &&\n FREE_TEXT_PROPERTY.test(property) &&\n !builtIn\n ) {\n sites.push({ key: `${file.path}#${declaration}`, line: lineOfNode(file, node) });\n }\n }\n ts.forEachChild(node, visit);\n };\n visit(file.sourceFile);\n return sites;\n}\n\nfunction compareReviewed(\n sites: readonly Site[],\n reviewed: ReviewedCounts,\n what: string,\n): Finding[] {\n const findings: Finding[] = [];\n const byKey = new Map<string, Site[]>();\n for (const site of sites) byKey.set(site.key, [...(byKey.get(site.key) ?? []), site]);\n const allowed = new Map(reviewed.map((e) => [`${e.file}#${e.declaration}`, e.count]));\n for (const [key, found] of byKey) {\n const count = allowed.get(key) ?? 0;\n const file = key.slice(0, key.indexOf('#'));\n for (const site of found.slice(count))\n findings.push({\n file,\n line: site.line,\n detail: `${what} in ${key.slice(key.indexOf('#') + 1)}${count ? ` beyond the ${count} reviewed` : ''}`,\n });\n }\n for (const [key, count] of allowed) {\n const found = byKey.get(key)?.length ?? 0;\n if (found < count)\n findings.push({\n file: key.slice(0, key.indexOf('#')),\n detail: `stale reviewed count for ${key}: reviewed ${count}, found ${found}`,\n });\n }\n return findings;\n}\n\ntype Declaration = { format?: unknown; min?: number; keepAsTyped?: unknown };\n\nconst FREE_TEXT_FORMAT = 'none';\nconst textDeclarations = (exports: Readonly<Record<string, unknown>>) =>\n Object.entries(exports).filter(\n (e): e is [string, Declaration] =>\n typeof e[1] === 'object' && e[1] !== null && 'format' in e[1],\n );\n\nconst reviewedCountProblems = (value: unknown, what: string, problems: string[]) =>\n reviewedProblems(value, what, problems, {\n key: 'file',\n id: (e) =>\n typeof e.declaration === 'string' && e.declaration.trim()\n ? `${e.file}#${e.declaration}`\n : undefined,\n idNames: 'declaration',\n extra(entry, label) {\n if (!isCount(entry.count)) problems.push(`${label}'s count must be a positive whole number`);\n },\n });\n\nexport const textFieldDeclarationsGuard: GenericGuardDefinition<TextFieldDeclarationFacts> = {\n id: 'textFieldDeclarations',\n absentWhen: \"the app's core declares no text field through input-format-core\",\n factProblems(facts) {\n const problems: string[] = [];\n namesProblem(facts?.wireFolders, 'wireFolders', problems);\n textProblem(facts?.declarationsFile, 'declarationsFile', problems);\n namesProblem(facts?.wireInputNameSuffixes, 'wireInputNameSuffixes', problems);\n reviewedCountProblems(facts?.selfBounded, 'selfBounded', problems);\n reviewedCountProblems(facts?.undeclaredFreeText, 'undeclaredFreeText', problems);\n reviewedProblems(facts?.keepAsTyped, 'keepAsTyped', problems, { key: 'declaration' });\n loaderProblem(facts?.declarations, 'declarations', problems);\n loaderProblem(facts?.fieldSchema, 'fieldSchema', problems);\n return problems;\n },\n build(facts) {\n const isWireInput = (name: string) => facts.wireInputNameSuffixes.some((s) => name.endsWith(s));\n const wireFiles = (context: GuardContext, findings: Finding[]) =>\n filesIn(context.files(CORE_CODE), facts.wireFolders, findings);\n return [\n {\n title: 'every free-text property of a core wire input is a declared text field',\n description:\n 'A wire input property that names free text (reason, message, title, …) reads its declared text field through ' +\n \"the core's field-schema builder, or carries a built-in format (.regex / .url / .email / .uuid). Wire text no \" +\n 'person types is a reviewed module#declaration count with why; a count that no longer matches fails.',\n timeoutMs: REPOSITORY_GUARD_TIMEOUT_MS,\n run(context) {\n const findings: Finding[] = [];\n const sites = wireFiles(context, findings).flatMap((f) =>\n undeclaredFreeText(f, isWireInput),\n );\n return [\n ...findings,\n ...compareReviewed(\n sites,\n facts.undeclaredFreeText,\n 'an undeclared free-text z.string() property',\n ),\n ];\n },\n },\n {\n title: 'no core wire schema trims or bounds a text field itself',\n description:\n 'A wire z.string() chain never trims itself or bounds itself by a text-length constant: that is a second ' +\n \"declaration of the field. Read the field's declaration through the core's field-schema builder. Wire text no \" +\n 'person types (stored values, fixed strings, ids, addresses) is a reviewed module#declaration count with why.',\n timeoutMs: REPOSITORY_GUARD_TIMEOUT_MS,\n run(context) {\n const findings: Finding[] = [];\n const sites = wireFiles(context, findings).flatMap(selfBoundedChains);\n return [\n ...findings,\n ...compareReviewed(sites, facts.selfBounded, 'a self-bounded z.string() chain'),\n ];\n },\n },\n {\n title: 'text fields are declared only in the core declarations module',\n description:\n 'defineInputFormat is called, and keepAsTyped is set, only in the one module that declares every text field, ' +\n 'so each field has one declaration. Move the declaration there and import it.',\n timeoutMs: REPOSITORY_GUARD_TIMEOUT_MS,\n run(context) {\n const findings: Finding[] = [];\n const files = context.files(CORE_CODE);\n if (!files.some((f) => f.path === facts.declarationsFile)) {\n findings.push({\n file: facts.declarationsFile,\n detail: 'the declarations module is not in the core build',\n });\n }\n for (const file of files) {\n if (file.path === facts.declarationsFile) continue;\n for (const m of file.code.matchAll(/\\bdefineInputFormat\\s*\\(|\\bkeepAsTyped\\b/g)) {\n findings.push({\n file: file.path,\n line: file.lineOf(m.index ?? 0),\n detail: `${m[0].replace(/\\s*\\($/, '')} outside the declarations module`,\n });\n }\n }\n return findings;\n },\n },\n {\n title: 'only the reviewed text fields keep their text as typed',\n description:\n 'Every declared text field is trimmed unless it is a reviewed keep-as-typed declaration with why (a legal or ' +\n 'statutory text whose exact characters matter). A declaration that newly keeps its text fails until reviewed, ' +\n 'and a reviewed one that no longer does is stale.',\n async run(context) {\n const findings: Finding[] = [];\n coreBuild(context, findings);\n const kept = new Set(\n textDeclarations(await facts.declarations())\n .filter(([, d]) => 'keepAsTyped' in d)\n .map(([n]) => n),\n );\n const reviewed = new Set(facts.keepAsTyped.map((e) => e.declaration));\n for (const name of [...kept].sort())\n if (!reviewed.has(name))\n findings.push({\n file: `(core) ${name}`,\n detail: 'keeps its text as typed without review',\n });\n for (const name of reviewed)\n if (!kept.has(name))\n findings.push({\n file: `(core) ${name}`,\n detail:\n 'stale keep-as-typed entry: the declaration no longer keeps its text, or is gone',\n });\n return findings;\n },\n },\n {\n title:\n 'every declared text field trims, one that holds words keeps one space for each run of spaces, and a required one refuses blank text',\n description:\n \"Each text field's wire schema, built from its declaration, refuses whitespace-only text when the field requires \" +\n 'text (min of 1 or more), and trims the text of every field that does not keep it as typed; a field whose format ' +\n \"holds words (every format but free text, 'none') keeps one space for a run of them — so the form, the wire, and the writer agree, and a \" +\n 'name spaced twice is never stored, searched, or reserved apart from the same name spaced once.',\n async run(context) {\n const findings: Finding[] = [];\n coreBuild(context, findings);\n const declarations = textDeclarations(await facts.declarations());\n const reader = await facts.fieldSchema();\n if (!declarations.length)\n findings.push({\n file: facts.declarationsFile,\n detail: 'no text-field declaration was loaded',\n });\n for (const [name, declaration] of declarations) {\n const schema = reader.schemaOf(declaration);\n const min = typeof declaration.min === 'number' ? declaration.min : 0;\n if (min >= 1 && schema.safeParse(' \\n\\t ').success)\n findings.push({\n file: `(core) ${name}`,\n detail: 'accepts whitespace-only text although the field requires text',\n });\n if ('keepAsTyped' in declaration) continue;\n const sample = 'a'.repeat(Math.max(min, 1));\n const parsed = schema.safeParse(` ${sample} `);\n if (!parsed.success || parsed.data !== sample)\n findings.push({ file: `(core) ${name}`, detail: 'does not trim its text' });\n if (declaration.format === FREE_TEXT_FORMAT) continue;\n const word = 'a'.repeat(Math.max(min - 2, 1));\n if (!schema.safeParse(`${word} a`).success) continue;\n const run = schema.safeParse(`${word} a`);\n if (!run.success || run.data !== `${word} a`)\n findings.push({\n file: `(core) ${name}`,\n detail: 'keeps a run of spaces instead of one space',\n });\n }\n return findings;\n },\n },\n ];\n },\n};\n"]}
@@ -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;AAsCD,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
+ {"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
- const { app, functions, sharedSource } = reader.layout.roots;
19
- return [app.path, ...[functions, sharedSource].flatMap((root) => (root ? [root.path] : []))];
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":"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","sourcesContent":["import type { Finding, GuardContext } from '../../guard.js';\nimport type { RepoReader, Scope } from '../../node/reader.js';\nimport { 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'],\n kinds: ['code'],\n tests: true,\n optionalRoots: ['functions'],\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 const { app, functions, sharedSource } = reader.layout.roots;\n return [app.path, ...[functions, sharedSource].flatMap((root) => (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
+ {"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 declare function retriedActions(source: FileView | string, helpers?: readonly string[]): Occurrence[];
5
- export declare function swallowedActions(source: FileView | string, helpers?: readonly string[]): Occurrence[];
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":"AACA,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,4BAA4B,CAAC;AAEzE,OAAO,EAAc,KAAK,QAAQ,EAAE,MAAM,sBAAsB,CAAC;AAEjE,OAAO,EAGL,KAAK,aAAa,EAClB,KAAK,UAAU,EAChB,MAAM,kCAAkC,CAAC;AAuE1C,wBAAgB,cAAc,CAC5B,MAAM,EAAE,QAAQ,GAAG,MAAM,EACzB,OAAO,GAAE,SAAS,MAAM,EAAO,GAC9B,UAAU,EAAE,CAsBd;AAwBD,wBAAgB,gBAAgB,CAC9B,MAAM,EAAE,QAAQ,GAAG,MAAM,EACzB,OAAO,GAAE,SAAS,MAAM,EAAO,GAC9B,UAAU,EAAE,CAkDd;AAED,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,CA4BvE,CAAC;AAEF,eAAO,MAAM,oBAAoB,EAAE,sBAAsB,CAAC,cAAc,CA4BvE,CAAC"}
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
- export function retriedActions(source, helpers = []) {
70
- const file = typeof source === 'string' ? viewOfText('sample.spec.ts', source) : source;
71
- const sf = file.sourceFile;
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
- return retriedCallbacks(sf).flatMap((fn) => {
74
- let action;
75
- forEachNode(fn.body, (node) => {
76
- if (!action && ts.isCallExpression(node)) {
77
- const name = calleeName(node);
78
- if (name && actions.has(name))
79
- action = name;
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 action
83
- ? [
84
- {
85
- start: fn.getStart(sf),
86
- end: fn.getEnd(),
87
- detail: `a retry loop repeats ${action}() until it passes`,
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 caughtCallOf(catchCall, names) {
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
- if (!ts.isCallExpression(call))
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 = typeof source === 'string' ? viewOfText('sample.spec.ts', source) : source;
224
+ export function swallowedActions(source, helpers = [], reach) {
225
+ const file = viewOf(source);
116
226
  const sf = file.sourceFile;
117
- const actions = actionNamesOf(helpers);
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 action = caughtCallOf(node, actions);
126
- const probe = caughtCallOf(node, STATE_PROBES);
127
- if (action)
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 ${action}()`,
244
+ detail: `a .catch() swallows a failed ${describeAction(chain)}`,
132
245
  });
133
- else if (probe)
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 ${probe}() — an ambiguous locator included — into a value`,
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
- let action;
149
- forEachNode(node.tryBlock, (inner) => {
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 ${action}()`,
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 repeats the action until it ' +
184
- 'happens to work, so the failure a real user would hit passes as green (RULE-STD-QUALITY-003). Act once, ' +
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. A loop that must act — " +
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 lets the next step run on a ' +
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"]}
@@ -2,6 +2,7 @@ export interface VitestAssertion {
2
2
  status?: string;
3
3
  title?: string;
4
4
  fullName?: string;
5
+ duration?: number | null;
5
6
  failureMessages?: string[];
6
7
  }
7
8
  export interface VitestFileResult {
@@ -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;AAID,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,CAgCA;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"}
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({ file: file.name, title, messages: a.failureMessages ?? [] });
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":"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","sourcesContent":["export interface VitestAssertion {\n status?: string;\n title?: string;\n fullName?: string;\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\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({ file: file.name, title, messages: a.failureMessages ?? [] });\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"]}
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"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ttt-productions/repo-guards",
3
- "version": "0.1.22",
3
+ "version": "0.1.24",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/ttt-productions/ttt-packages.git",