@ttt-productions/repo-guards 0.1.6 → 0.1.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/modules/core-definitions/text-field-declarations.d.ts.map +1 -1
- package/dist/modules/core-definitions/text-field-declarations.js +10 -3
- package/dist/modules/core-definitions/text-field-declarations.js.map +1 -1
- package/dist/modules/import-paths/guards.d.ts +2 -0
- package/dist/modules/import-paths/guards.d.ts.map +1 -1
- package/dist/modules/import-paths/guards.js +2 -0
- package/dist/modules/import-paths/guards.js.map +1 -1
- package/dist/modules/import-paths/package-wildcard-imports.d.ts +7 -0
- package/dist/modules/import-paths/package-wildcard-imports.d.ts.map +1 -0
- package/dist/modules/import-paths/package-wildcard-imports.js +56 -0
- package/dist/modules/import-paths/package-wildcard-imports.js.map +1 -0
- package/dist/modules/repo/guards.d.ts +2 -0
- package/dist/modules/repo/guards.d.ts.map +1 -1
- package/dist/modules/repo/guards.js +2 -0
- package/dist/modules/repo/guards.js.map +1 -1
- package/dist/modules/repo/test-case-module-loads.d.ts +28 -0
- package/dist/modules/repo/test-case-module-loads.d.ts.map +1 -0
- package/dist/modules/repo/test-case-module-loads.js +128 -0
- package/dist/modules/repo/test-case-module-loads.js.map +1 -0
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"text-field-declarations.d.ts","sourceRoot":"","sources":["../../../src/modules/core-definitions/text-field-declarations.ts"],"names":[],"mappings":"AAQA,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;AAGhC,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;AAE3F,MAAM,WAAW,yBAAyB;IACxC,qEAAqE;IACrE,WAAW,EAAE,SAAS,MAAM,EAAE,CAAC;IAC/B,qFAAqF;IACrF,gBAAgB,EAAE,MAAM,CAAC;IACzB,kGAAkG;IAClG,qBAAqB,EAAE,SAAS,MAAM,EAAE,CAAC;IACzC,8GAA8G;IAC9G,WAAW,EAAE,cAAc,CAAC;IAC5B,sGAAsG;IACtG,kBAAkB,EAAE,cAAc,CAAC;IACnC,qEAAqE;IACrE,WAAW,EAAE,aAAa,CAAC;QAAE,WAAW,EAAE,MAAM,CAAC;QAAC,GAAG,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IACjE,yCAAyC;IACzC,YAAY,EAAE,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC,CAAC;IACxD,gEAAgE;IAChE,WAAW,EAAE,MAAM,CAAC,CAAC,WAAW,EAAE,KAAK,KAAK,WAAW,CAAC,CAAC;CAC1D;AAiCD,UAAU,IAAI;IACZ,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;CACd;AAED,6FAA6F;AAC7F,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,QAAQ,GAAG,IAAI,EAAE,CAyBxD;AAED,+GAA+G;AAC/G,wBAAgB,kBAAkB,CAChC,IAAI,EAAE,QAAQ,EACd,WAAW,EAAE,CAAC,WAAW,EAAE,MAAM,KAAK,OAAO,GAC5C,IAAI,EAAE,CAiCR;
|
|
1
|
+
{"version":3,"file":"text-field-declarations.d.ts","sourceRoot":"","sources":["../../../src/modules/core-definitions/text-field-declarations.ts"],"names":[],"mappings":"AAQA,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;AAGhC,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;AAE3F,MAAM,WAAW,yBAAyB;IACxC,qEAAqE;IACrE,WAAW,EAAE,SAAS,MAAM,EAAE,CAAC;IAC/B,qFAAqF;IACrF,gBAAgB,EAAE,MAAM,CAAC;IACzB,kGAAkG;IAClG,qBAAqB,EAAE,SAAS,MAAM,EAAE,CAAC;IACzC,8GAA8G;IAC9G,WAAW,EAAE,cAAc,CAAC;IAC5B,sGAAsG;IACtG,kBAAkB,EAAE,cAAc,CAAC;IACnC,qEAAqE;IACrE,WAAW,EAAE,aAAa,CAAC;QAAE,WAAW,EAAE,MAAM,CAAC;QAAC,GAAG,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IACjE,yCAAyC;IACzC,YAAY,EAAE,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC,CAAC;IACxD,gEAAgE;IAChE,WAAW,EAAE,MAAM,CAAC,CAAC,WAAW,EAAE,KAAK,KAAK,WAAW,CAAC,CAAC;CAC1D;AAiCD,UAAU,IAAI;IACZ,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;CACd;AAED,6FAA6F;AAC7F,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,QAAQ,GAAG,IAAI,EAAE,CAyBxD;AAED,+GAA+G;AAC/G,wBAAgB,kBAAkB,CAChC,IAAI,EAAE,QAAQ,EACd,WAAW,EAAE,CAAC,WAAW,EAAE,MAAM,KAAK,OAAO,GAC5C,IAAI,EAAE,CAiCR;AA2DD,eAAO,MAAM,0BAA0B,EAAE,sBAAsB,CAAC,yBAAyB,CAmKxF,CAAC"}
|
|
@@ -114,6 +114,11 @@ function compareReviewed(sites, reviewed, what) {
|
|
|
114
114
|
}
|
|
115
115
|
return findings;
|
|
116
116
|
}
|
|
117
|
+
/**
|
|
118
|
+
* input-format-core's format for free text — a description, a message, a chapter — which holds its
|
|
119
|
+
* spacing as typed; every other format holds a run of spaces to one.
|
|
120
|
+
*/
|
|
121
|
+
const FREE_TEXT_FORMAT = 'none';
|
|
117
122
|
const textDeclarations = (exports) => Object.entries(exports).filter((e) => typeof e[1] === 'object' && e[1] !== null && 'format' in e[1]);
|
|
118
123
|
const reviewedCountProblems = (value, what, problems) => reviewedProblems(value, what, problems, {
|
|
119
124
|
key: 'file',
|
|
@@ -231,10 +236,10 @@ export const textFieldDeclarationsGuard = {
|
|
|
231
236
|
},
|
|
232
237
|
},
|
|
233
238
|
{
|
|
234
|
-
title: 'every declared text field trims, keeps one space for each run of spaces, and a required one refuses blank text',
|
|
239
|
+
title: 'every declared text field trims, one that holds words keeps one space for each run of spaces, and a required one refuses blank text',
|
|
235
240
|
description: "Each text field's wire schema, built from its declaration, refuses whitespace-only text when the field requires " +
|
|
236
|
-
'text (min of 1 or more), and trims the text of every field that does not keep it as typed; a field
|
|
237
|
-
|
|
241
|
+
'text (min of 1 or more), and trims the text of every field that does not keep it as typed; a field whose format ' +
|
|
242
|
+
"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 " +
|
|
238
243
|
'name spaced twice is never stored, searched, or reserved apart from the same name spaced once.',
|
|
239
244
|
async run(context) {
|
|
240
245
|
const findings = [];
|
|
@@ -260,6 +265,8 @@ export const textFieldDeclarationsGuard = {
|
|
|
260
265
|
const parsed = schema.safeParse(` ${sample} `);
|
|
261
266
|
if (!parsed.success || parsed.data !== sample)
|
|
262
267
|
findings.push({ file: `(core) ${name}`, detail: 'does not trim its text' });
|
|
268
|
+
if (declaration.format === FREE_TEXT_FORMAT)
|
|
269
|
+
continue;
|
|
263
270
|
const word = 'a'.repeat(Math.max(min - 2, 1));
|
|
264
271
|
if (!schema.safeParse(`${word} a`).success)
|
|
265
272
|
continue;
|
|
@@ -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":["// Text-field declarations: every text field a person types is declared once in the core's\n// declarations module (its input format, min, and max, through input-format-core's\n// `defineInputFormat`) and every wire schema reads it through the core's field-schema builder, so\n// the trim, the non-blank rule, the cap, and the characters are the same at the form, the wire, and\n// the writer.\n\nimport 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';\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\nexport interface TextFieldDeclarationFacts {\n /** The core build folders that hold wire schemas (repo-relative). */\n wireFolders: readonly string[];\n /** The core build module that holds every text-field declaration (repo-relative). */\n declarationsFile: string;\n /** Export-name endings that mark a wire input declaration (`InputSchema`, `RequestSchema`, …). */\n wireInputNameSuffixes: readonly string[];\n /** Wire `z.string()` chains that trim or bound themselves, per `module#declaration`: text no person types. */\n selfBounded: ReviewedCounts;\n /** Free-text-named wire `z.string()` properties that are not typed text, per `module#declaration`. */\n undeclaredFreeText: ReviewedCounts;\n /** The declarations that keep their text as typed, each with why. */\n keepAsTyped: ReadonlyArray<{ declaration: string; why: string }>;\n /** The declarations module's exports. */\n declarations: Loader<Readonly<Record<string, unknown>>>;\n /** The core's builder of a wire schema from one declaration. */\n fieldSchema: Loader<(declaration: never) => FieldSchema>;\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\n/** A length constant that bounds text — an id's length (`*_ID_LENGTH`) bounds an id, not text. */\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\n/** The method calls chained onto a `z.string()` call, outermost last. */\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\n/** Every `z.string()` chain that trims itself or bounds itself by a text-length constant. */\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\n/** Every free-text-named `z.string()` property of a wire input declaration that carries no built-in format. */\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\n/** Sites against reviewed `module#declaration` counts: an excess site fails, a short count is stale. */\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 };\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: 120_000,\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: 120_000,\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: 120_000,\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, 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 that accepts ' +\n 'a space between words 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 as never);\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 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":["// Text-field declarations: every text field a person types is declared once in the core's\n// declarations module (its input format, min, and max, through input-format-core's\n// `defineInputFormat`) and every wire schema reads it through the core's field-schema builder, so\n// the trim, the non-blank rule, the cap, and the characters are the same at the form, the wire, and\n// the writer.\n\nimport 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';\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\nexport interface TextFieldDeclarationFacts {\n /** The core build folders that hold wire schemas (repo-relative). */\n wireFolders: readonly string[];\n /** The core build module that holds every text-field declaration (repo-relative). */\n declarationsFile: string;\n /** Export-name endings that mark a wire input declaration (`InputSchema`, `RequestSchema`, …). */\n wireInputNameSuffixes: readonly string[];\n /** Wire `z.string()` chains that trim or bound themselves, per `module#declaration`: text no person types. */\n selfBounded: ReviewedCounts;\n /** Free-text-named wire `z.string()` properties that are not typed text, per `module#declaration`. */\n undeclaredFreeText: ReviewedCounts;\n /** The declarations that keep their text as typed, each with why. */\n keepAsTyped: ReadonlyArray<{ declaration: string; why: string }>;\n /** The declarations module's exports. */\n declarations: Loader<Readonly<Record<string, unknown>>>;\n /** The core's builder of a wire schema from one declaration. */\n fieldSchema: Loader<(declaration: never) => FieldSchema>;\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\n/** A length constant that bounds text — an id's length (`*_ID_LENGTH`) bounds an id, not text. */\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\n/** The method calls chained onto a `z.string()` call, outermost last. */\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\n/** Every `z.string()` chain that trims itself or bounds itself by a text-length constant. */\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\n/** Every free-text-named `z.string()` property of a wire input declaration that carries no built-in format. */\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\n/** Sites against reviewed `module#declaration` counts: an excess site fails, a short count is stale. */\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\n/**\n * input-format-core's format for free text — a description, a message, a chapter — which holds its\n * spacing as typed; every other format holds a run of spaces to one.\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: 120_000,\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: 120_000,\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: 120_000,\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 as never);\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"]}
|
|
@@ -3,12 +3,14 @@ import { type ApplicationDataFacts } from './application-data.js';
|
|
|
3
3
|
import { type EntryPurityFacts } from './entry-purity.js';
|
|
4
4
|
import { type ImportPathFacts } from './guard.js';
|
|
5
5
|
import { type PackageEntryFacts, type PackageEntryImportFacts } from './package-entries.js';
|
|
6
|
+
import { type PackageWildcardImportFacts } from './package-wildcard-imports.js';
|
|
6
7
|
import { type PackageImportFacts } from './package-imports.js';
|
|
7
8
|
/** The facts each guard in this area needs, by its id. */
|
|
8
9
|
export interface ImportPathGuardFacts {
|
|
9
10
|
importPaths: ImportPathFacts;
|
|
10
11
|
packageEntries: PackageEntryFacts;
|
|
11
12
|
packageEntryImports: PackageEntryImportFacts;
|
|
13
|
+
packageWildcardImports: PackageWildcardImportFacts;
|
|
12
14
|
packageImports: PackageImportFacts;
|
|
13
15
|
packageImportCycles: PackageImportFacts;
|
|
14
16
|
entryPurity: EntryPurityFacts;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"guards.d.ts","sourceRoot":"","sources":["../../../src/modules/import-paths/guards.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,4BAA4B,CAAC;AACzE,OAAO,EAA+B,KAAK,oBAAoB,EAAE,MAAM,uBAAuB,CAAC;AAC/F,OAAO,EAAoB,KAAK,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AAC5E,OAAO,EAAmB,KAAK,eAAe,EAAE,MAAM,YAAY,CAAC;AACnE,OAAO,EAGL,KAAK,iBAAiB,EACtB,KAAK,uBAAuB,EAC7B,MAAM,sBAAsB,CAAC;AAC9B,OAAO,EAGL,KAAK,kBAAkB,EACxB,MAAM,sBAAsB,CAAC;AAE9B,0DAA0D;AAC1D,MAAM,WAAW,oBAAoB;IACnC,WAAW,EAAE,eAAe,CAAC;IAC7B,cAAc,EAAE,iBAAiB,CAAC;IAClC,mBAAmB,EAAE,uBAAuB,CAAC;IAC7C,cAAc,EAAE,kBAAkB,CAAC;IACnC,mBAAmB,EAAE,kBAAkB,CAAC;IACxC,WAAW,EAAE,gBAAgB,CAAC;IAC9B,sBAAsB,EAAE,oBAAoB,CAAC;CAC9C;AAED,eAAO,MAAM,kBAAkB,EAAE,aAAa,CAAC,sBAAsB,CAAC,KAAK,CAAC,
|
|
1
|
+
{"version":3,"file":"guards.d.ts","sourceRoot":"","sources":["../../../src/modules/import-paths/guards.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,4BAA4B,CAAC;AACzE,OAAO,EAA+B,KAAK,oBAAoB,EAAE,MAAM,uBAAuB,CAAC;AAC/F,OAAO,EAAoB,KAAK,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AAC5E,OAAO,EAAmB,KAAK,eAAe,EAAE,MAAM,YAAY,CAAC;AACnE,OAAO,EAGL,KAAK,iBAAiB,EACtB,KAAK,uBAAuB,EAC7B,MAAM,sBAAsB,CAAC;AAC9B,OAAO,EAEL,KAAK,0BAA0B,EAChC,MAAM,+BAA+B,CAAC;AACvC,OAAO,EAGL,KAAK,kBAAkB,EACxB,MAAM,sBAAsB,CAAC;AAE9B,0DAA0D;AAC1D,MAAM,WAAW,oBAAoB;IACnC,WAAW,EAAE,eAAe,CAAC;IAC7B,cAAc,EAAE,iBAAiB,CAAC;IAClC,mBAAmB,EAAE,uBAAuB,CAAC;IAC7C,sBAAsB,EAAE,0BAA0B,CAAC;IACnD,cAAc,EAAE,kBAAkB,CAAC;IACnC,mBAAmB,EAAE,kBAAkB,CAAC;IACxC,WAAW,EAAE,gBAAgB,CAAC;IAC9B,sBAAsB,EAAE,oBAAoB,CAAC;CAC9C;AAED,eAAO,MAAM,kBAAkB,EAAE,aAAa,CAAC,sBAAsB,CAAC,KAAK,CAAC,CAS1B,CAAC"}
|
|
@@ -3,11 +3,13 @@ import { applicationDataImportsGuard } from './application-data.js';
|
|
|
3
3
|
import { entryPurityGuard } from './entry-purity.js';
|
|
4
4
|
import { importPathGuard } from './guard.js';
|
|
5
5
|
import { packageEntriesGuard, packageEntryImportsGuard, } from './package-entries.js';
|
|
6
|
+
import { packageWildcardImportsGuard, } from './package-wildcard-imports.js';
|
|
6
7
|
import { packageImportCyclesGuard, packageImportsGuard, } from './package-imports.js';
|
|
7
8
|
export const IMPORT_PATH_GUARDS = Object.freeze([
|
|
8
9
|
importPathGuard,
|
|
9
10
|
packageEntriesGuard,
|
|
10
11
|
packageEntryImportsGuard,
|
|
12
|
+
packageWildcardImportsGuard,
|
|
11
13
|
packageImportsGuard,
|
|
12
14
|
packageImportCyclesGuard,
|
|
13
15
|
entryPurityGuard,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"guards.js","sourceRoot":"","sources":["../../../src/modules/import-paths/guards.ts"],"names":[],"mappings":"AAAA,uEAAuE;AAGvE,OAAO,EAAE,2BAA2B,EAA6B,MAAM,uBAAuB,CAAC;AAC/F,OAAO,EAAE,gBAAgB,EAAyB,MAAM,mBAAmB,CAAC;AAC5E,OAAO,EAAE,eAAe,EAAwB,MAAM,YAAY,CAAC;AACnE,OAAO,EACL,mBAAmB,EACnB,wBAAwB,GAGzB,MAAM,sBAAsB,CAAC;AAC9B,OAAO,EACL,wBAAwB,EACxB,mBAAmB,GAEpB,MAAM,sBAAsB,CAAC;
|
|
1
|
+
{"version":3,"file":"guards.js","sourceRoot":"","sources":["../../../src/modules/import-paths/guards.ts"],"names":[],"mappings":"AAAA,uEAAuE;AAGvE,OAAO,EAAE,2BAA2B,EAA6B,MAAM,uBAAuB,CAAC;AAC/F,OAAO,EAAE,gBAAgB,EAAyB,MAAM,mBAAmB,CAAC;AAC5E,OAAO,EAAE,eAAe,EAAwB,MAAM,YAAY,CAAC;AACnE,OAAO,EACL,mBAAmB,EACnB,wBAAwB,GAGzB,MAAM,sBAAsB,CAAC;AAC9B,OAAO,EACL,2BAA2B,GAE5B,MAAM,+BAA+B,CAAC;AACvC,OAAO,EACL,wBAAwB,EACxB,mBAAmB,GAEpB,MAAM,sBAAsB,CAAC;AAc9B,MAAM,CAAC,MAAM,kBAAkB,GAAiD,MAAM,CAAC,MAAM,CAAC;IAC5F,eAAe;IACf,mBAAmB;IACnB,wBAAwB;IACxB,2BAA2B;IAC3B,mBAAmB;IACnB,wBAAwB;IACxB,gBAAgB;IAChB,2BAA2B;CACoB,CAAC,CAAC","sourcesContent":["// The import-path guard: every definition has exactly one import path.\n\nimport type { GenericGuardDefinition } from '../../node/suite/config.js';\nimport { applicationDataImportsGuard, type ApplicationDataFacts } from './application-data.js';\nimport { entryPurityGuard, type EntryPurityFacts } from './entry-purity.js';\nimport { importPathGuard, type ImportPathFacts } from './guard.js';\nimport {\n packageEntriesGuard,\n packageEntryImportsGuard,\n type PackageEntryFacts,\n type PackageEntryImportFacts,\n} from './package-entries.js';\nimport {\n packageWildcardImportsGuard,\n type PackageWildcardImportFacts,\n} from './package-wildcard-imports.js';\nimport {\n packageImportCyclesGuard,\n packageImportsGuard,\n type PackageImportFacts,\n} from './package-imports.js';\n\n/** The facts each guard in this area needs, by its id. */\nexport interface ImportPathGuardFacts {\n importPaths: ImportPathFacts;\n packageEntries: PackageEntryFacts;\n packageEntryImports: PackageEntryImportFacts;\n packageWildcardImports: PackageWildcardImportFacts;\n packageImports: PackageImportFacts;\n packageImportCycles: PackageImportFacts;\n entryPurity: EntryPurityFacts;\n applicationDataImports: ApplicationDataFacts;\n}\n\nexport const IMPORT_PATH_GUARDS: ReadonlyArray<GenericGuardDefinition<never>> = Object.freeze([\n importPathGuard,\n packageEntriesGuard,\n packageEntryImportsGuard,\n packageWildcardImportsGuard,\n packageImportsGuard,\n packageImportCyclesGuard,\n entryPurityGuard,\n applicationDataImportsGuard,\n] as ReadonlyArray<GenericGuardDefinition<never>>);\n"]}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { GenericGuardDefinition } from '../../node/suite/config.js';
|
|
2
|
+
export interface PackageWildcardImportFacts {
|
|
3
|
+
/** The npm scope whose packages the rule holds (`@ttt-productions`). */
|
|
4
|
+
scope: string;
|
|
5
|
+
}
|
|
6
|
+
export declare const packageWildcardImportsGuard: GenericGuardDefinition<PackageWildcardImportFacts>;
|
|
7
|
+
//# sourceMappingURL=package-wildcard-imports.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"package-wildcard-imports.d.ts","sourceRoot":"","sources":["../../../src/modules/import-paths/package-wildcard-imports.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,4BAA4B,CAAC;AAGzE,MAAM,WAAW,0BAA0B;IACzC,wEAAwE;IACxE,KAAK,EAAE,MAAM,CAAC;CACf;AASD,eAAO,MAAM,2BAA2B,EAAE,sBAAsB,CAAC,0BAA0B,CA6C1F,CAAC"}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
// No wildcard import of a scoped package. A consumer that takes a package's whole module (`import *
|
|
2
|
+
// as X`, `export * as X from`, `export * from`) names no definition, so nothing traces a name it uses
|
|
3
|
+
// to the entry that owns it, and a test's mock of one name stands in for every other name the
|
|
4
|
+
// wildcard reaches. Each name is imported by name instead.
|
|
5
|
+
import { textProblem } from '../../support/facts.js';
|
|
6
|
+
const SCOPE = {
|
|
7
|
+
roots: ['app', 'functions', 'e2e', 'hostedE2e', 'scripts', 'workers'],
|
|
8
|
+
kinds: ['code'],
|
|
9
|
+
tests: true,
|
|
10
|
+
optionalRoots: ['functions', 'e2e', 'hostedE2e', 'scripts', 'workers'],
|
|
11
|
+
};
|
|
12
|
+
export const packageWildcardImportsGuard = {
|
|
13
|
+
id: 'packageWildcardImports',
|
|
14
|
+
absentWhen: false,
|
|
15
|
+
factProblems(facts) {
|
|
16
|
+
const problems = [];
|
|
17
|
+
textProblem(facts?.scope, 'scope', problems);
|
|
18
|
+
if (typeof facts?.scope === 'string' && !/^@[a-z0-9][a-z0-9._-]*$/.test(facts.scope))
|
|
19
|
+
problems.push('scope must be an npm scope such as @acme');
|
|
20
|
+
return problems;
|
|
21
|
+
},
|
|
22
|
+
build(facts) {
|
|
23
|
+
const prefix = `${facts.scope}/`;
|
|
24
|
+
return [
|
|
25
|
+
{
|
|
26
|
+
title: 'no code takes a scoped package whole through a wildcard import',
|
|
27
|
+
description: "No code imports or re-exports a scoped package's whole module (`import * as X`, `export * as X from`, " +
|
|
28
|
+
'`export * from`): a wildcard names no definition, so no name it uses is traced to the entry that owns it, and a ' +
|
|
29
|
+
"test's mock of one name stands in for the rest. Import each name the code uses, by name.",
|
|
30
|
+
run(context) {
|
|
31
|
+
const findings = [];
|
|
32
|
+
for (const file of context.files(SCOPE)) {
|
|
33
|
+
for (const imported of file.imports) {
|
|
34
|
+
if (imported.kind !== 'import' && imported.kind !== 'export')
|
|
35
|
+
continue;
|
|
36
|
+
if (!imported.star || !imported.spec.startsWith(prefix))
|
|
37
|
+
continue;
|
|
38
|
+
const form = imported.kind === 'import'
|
|
39
|
+
? `import * as ${imported.namespace}`
|
|
40
|
+
: imported.namespace
|
|
41
|
+
? `export * as ${imported.namespace}`
|
|
42
|
+
: 'export *';
|
|
43
|
+
findings.push({
|
|
44
|
+
file: file.path,
|
|
45
|
+
line: file.lineOf(imported.node.getStart(file.sourceFile)),
|
|
46
|
+
detail: `${form} from '${imported.spec}' takes the whole module; import each name it uses`,
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return findings.sort((a, b) => a.file === b.file ? (a.line ?? 0) - (b.line ?? 0) : a.file < b.file ? -1 : 1);
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
];
|
|
54
|
+
},
|
|
55
|
+
};
|
|
56
|
+
//# sourceMappingURL=package-wildcard-imports.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"package-wildcard-imports.js","sourceRoot":"","sources":["../../../src/modules/import-paths/package-wildcard-imports.ts"],"names":[],"mappings":"AAAA,oGAAoG;AACpG,sGAAsG;AACtG,8FAA8F;AAC9F,2DAA2D;AAK3D,OAAO,EAAE,WAAW,EAAE,MAAM,wBAAwB,CAAC;AAOrD,MAAM,KAAK,GAAU;IACnB,KAAK,EAAE,CAAC,KAAK,EAAE,WAAW,EAAE,KAAK,EAAE,WAAW,EAAE,SAAS,EAAE,SAAS,CAAC;IACrE,KAAK,EAAE,CAAC,MAAM,CAAC;IACf,KAAK,EAAE,IAAI;IACX,aAAa,EAAE,CAAC,WAAW,EAAE,KAAK,EAAE,WAAW,EAAE,SAAS,EAAE,SAAS,CAAC;CACvE,CAAC;AAEF,MAAM,CAAC,MAAM,2BAA2B,GAAuD;IAC7F,EAAE,EAAE,wBAAwB;IAC5B,UAAU,EAAE,KAAK;IACjB,YAAY,CAAC,KAAK;QAChB,MAAM,QAAQ,GAAa,EAAE,CAAC;QAC9B,WAAW,CAAC,KAAK,EAAE,KAAK,EAAE,OAAO,EAAE,QAAQ,CAAC,CAAC;QAC7C,IAAI,OAAO,KAAK,EAAE,KAAK,KAAK,QAAQ,IAAI,CAAC,yBAAyB,CAAC,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC;YAClF,QAAQ,CAAC,IAAI,CAAC,0CAA0C,CAAC,CAAC;QAC5D,OAAO,QAAQ,CAAC;IAClB,CAAC;IACD,KAAK,CAAC,KAAK;QACT,MAAM,MAAM,GAAG,GAAG,KAAK,CAAC,KAAK,GAAG,CAAC;QACjC,OAAO;YACL;gBACE,KAAK,EAAE,gEAAgE;gBACvE,WAAW,EACT,wGAAwG;oBACxG,kHAAkH;oBAClH,0FAA0F;gBAC5F,GAAG,CAAC,OAAqB;oBACvB,MAAM,QAAQ,GAAc,EAAE,CAAC;oBAC/B,KAAK,MAAM,IAAI,IAAI,OAAO,CAAC,KAAK,CAAC,KAAK,CAAC,EAAE,CAAC;wBACxC,KAAK,MAAM,QAAQ,IAAI,IAAI,CAAC,OAAO,EAAE,CAAC;4BACpC,IAAI,QAAQ,CAAC,IAAI,KAAK,QAAQ,IAAI,QAAQ,CAAC,IAAI,KAAK,QAAQ;gCAAE,SAAS;4BACvE,IAAI,CAAC,QAAQ,CAAC,IAAI,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,UAAU,CAAC,MAAM,CAAC;gCAAE,SAAS;4BAClE,MAAM,IAAI,GACR,QAAQ,CAAC,IAAI,KAAK,QAAQ;gCACxB,CAAC,CAAC,eAAe,QAAQ,CAAC,SAAS,EAAE;gCACrC,CAAC,CAAC,QAAQ,CAAC,SAAS;oCAClB,CAAC,CAAC,eAAe,QAAQ,CAAC,SAAS,EAAE;oCACrC,CAAC,CAAC,UAAU,CAAC;4BACnB,QAAQ,CAAC,IAAI,CAAC;gCACZ,IAAI,EAAE,IAAI,CAAC,IAAI;gCACf,IAAI,EAAE,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC;gCAC1D,MAAM,EAAE,GAAG,IAAI,UAAU,QAAQ,CAAC,IAAI,oDAAoD;6BAC3F,CAAC,CAAC;wBACL,CAAC;oBACH,CAAC;oBACD,OAAO,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAC5B,CAAC,CAAC,IAAI,KAAK,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,GAAG,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAC7E,CAAC;gBACJ,CAAC;aACF;SACF,CAAC;IACJ,CAAC;CACF,CAAC","sourcesContent":["// No wildcard import of a scoped package. A consumer that takes a package's whole module (`import *\n// as X`, `export * as X from`, `export * from`) names no definition, so nothing traces a name it uses\n// to the entry that owns it, and a test's mock of one name stands in for every other name the\n// wildcard reaches. Each name is imported by name instead.\n\nimport type { Finding, GuardContext } from '../../guard.js';\nimport type { Scope } from '../../node/reader.js';\nimport type { GenericGuardDefinition } from '../../node/suite/config.js';\nimport { textProblem } from '../../support/facts.js';\n\nexport interface PackageWildcardImportFacts {\n /** The npm scope whose packages the rule holds (`@ttt-productions`). */\n scope: 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\nexport const packageWildcardImportsGuard: GenericGuardDefinition<PackageWildcardImportFacts> = {\n id: 'packageWildcardImports',\n absentWhen: false,\n factProblems(facts) {\n const problems: string[] = [];\n textProblem(facts?.scope, 'scope', problems);\n if (typeof facts?.scope === 'string' && !/^@[a-z0-9][a-z0-9._-]*$/.test(facts.scope))\n problems.push('scope must be an npm scope such as @acme');\n return problems;\n },\n build(facts) {\n const prefix = `${facts.scope}/`;\n return [\n {\n title: 'no code takes a scoped package whole through a wildcard import',\n description:\n \"No code imports or re-exports a scoped package's whole module (`import * as X`, `export * as X from`, \" +\n '`export * from`): a wildcard names no definition, so no name it uses is traced to the entry that owns it, and a ' +\n \"test's mock of one name stands in for the rest. Import each name the code uses, by name.\",\n run(context: GuardContext): Finding[] {\n const findings: Finding[] = [];\n for (const file of context.files(SCOPE)) {\n for (const imported of file.imports) {\n if (imported.kind !== 'import' && imported.kind !== 'export') continue;\n if (!imported.star || !imported.spec.startsWith(prefix)) continue;\n const form =\n imported.kind === 'import'\n ? `import * as ${imported.namespace}`\n : imported.namespace\n ? `export * as ${imported.namespace}`\n : 'export *';\n findings.push({\n file: file.path,\n line: file.lineOf(imported.node.getStart(file.sourceFile)),\n detail: `${form} from '${imported.spec}' takes the whole module; import each name it uses`,\n });\n }\n }\n return findings.sort((a, b) =>\n a.file === b.file ? (a.line ?? 0) - (b.line ?? 0) : a.file < b.file ? -1 : 1,\n );\n },\n },\n ];\n },\n};\n"]}
|
|
@@ -9,6 +9,7 @@ import { type LocalOnlyVariablesFacts } from './local-only-variables.js';
|
|
|
9
9
|
import { type ProjectScriptFacts } from './project-scripts.js';
|
|
10
10
|
import { type SecretBindingFacts } from './secret-bindings.js';
|
|
11
11
|
import { type ShellSafeFacts } from './shell-safe.js';
|
|
12
|
+
import { type TestCaseModuleLoadFacts } from './test-case-module-loads.js';
|
|
12
13
|
import { type TestSourceRootFacts } from './test-source-root.js';
|
|
13
14
|
import { type WalkerReadFailureFacts } from './walker-read-failure.js';
|
|
14
15
|
import { type WorkerBoundedBodyFacts } from './worker-bounded-body.js';
|
|
@@ -18,6 +19,7 @@ export interface RepoGuardFacts {
|
|
|
18
19
|
shellSafeChildProcesses: ShellSafeFacts;
|
|
19
20
|
walkerReadFailure: WalkerReadFailureFacts;
|
|
20
21
|
testSourceRoot: TestSourceRootFacts | Absent;
|
|
22
|
+
testCaseModuleLoads: TestCaseModuleLoadFacts;
|
|
21
23
|
localOnlyVariables: LocalOnlyVariablesFacts;
|
|
22
24
|
workerConfigIsolation: WorkerConfigFacts | Absent;
|
|
23
25
|
workerBoundedBody: WorkerBoundedBodyFacts | Absent;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"guards.d.ts","sourceRoot":"","sources":["../../../src/modules/repo/guards.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,MAAM,EAAE,sBAAsB,EAAE,MAAM,4BAA4B,CAAC;AACjF,OAAO,EAKL,KAAK,eAAe,EACpB,KAAK,kBAAkB,EACvB,KAAK,sBAAsB,EAC3B,KAAK,kBAAkB,EACxB,MAAM,iBAAiB,CAAC;AACzB,OAAO,EAAiB,KAAK,aAAa,EAAE,MAAM,gBAAgB,CAAC;AACnE,OAAO,EAA2B,KAAK,uBAAuB,EAAE,MAAM,2BAA2B,CAAC;AAClG,OAAO,EAAsB,KAAK,kBAAkB,EAAE,MAAM,qBAAqB,CAAC;AAClF,OAAO,EAAwB,KAAK,oBAAoB,EAAE,MAAM,wBAAwB,CAAC;AACzF,OAAO,EAEL,KAAK,8BAA8B,EACpC,MAAM,kCAAkC,CAAC;AAC1C,OAAO,EAA2B,KAAK,uBAAuB,EAAE,MAAM,2BAA2B,CAAC;AAClG,OAAO,EAAsB,KAAK,kBAAkB,EAAE,MAAM,sBAAsB,CAAC;AACnF,OAAO,EAAsB,KAAK,kBAAkB,EAAE,MAAM,sBAAsB,CAAC;AACnF,OAAO,EAAkB,KAAK,cAAc,EAAE,MAAM,iBAAiB,CAAC;AACtE,OAAO,EAAuB,KAAK,mBAAmB,EAAE,MAAM,uBAAuB,CAAC;AACtF,OAAO,EAA0B,KAAK,sBAAsB,EAAE,MAAM,0BAA0B,CAAC;AAC/F,OAAO,EAA0B,KAAK,sBAAsB,EAAE,MAAM,0BAA0B,CAAC;AAC/F,OAAO,EAAqB,KAAK,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AAE/E,0DAA0D;AAC1D,MAAM,WAAW,cAAc;IAC7B,uBAAuB,EAAE,cAAc,CAAC;IACxC,iBAAiB,EAAE,sBAAsB,CAAC;IAC1C,cAAc,EAAE,mBAAmB,GAAG,MAAM,CAAC;IAC7C,kBAAkB,EAAE,uBAAuB,CAAC;IAC5C,qBAAqB,EAAE,iBAAiB,GAAG,MAAM,CAAC;IAClD,iBAAiB,EAAE,sBAAsB,GAAG,MAAM,CAAC;IACnD,aAAa,EAAE,kBAAkB,CAAC;IAClC,qBAAqB,EAAE,kBAAkB,CAAC;IAC1C,kBAAkB,EAAE,eAAe,CAAC;IACpC,iBAAiB,EAAE,sBAAsB,CAAC;IAC1C,cAAc,EAAE,kBAAkB,GAAG,MAAM,CAAC;IAC5C,cAAc,EAAE,kBAAkB,GAAG,MAAM,CAAC;IAC5C,aAAa,EAAE,kBAAkB,CAAC;IAClC,eAAe,EAAE,oBAAoB,GAAG,MAAM,CAAC;IAC/C,yBAAyB,EAAE,8BAA8B,CAAC;IAC1D,QAAQ,EAAE,aAAa,GAAG,MAAM,CAAC;IACjC,mBAAmB,EAAE,uBAAuB,GAAG,MAAM,CAAC;CACvD;AAED,eAAO,MAAM,WAAW,EAAE,aAAa,CAAC,sBAAsB,CAAC,KAAK,CAAC,
|
|
1
|
+
{"version":3,"file":"guards.d.ts","sourceRoot":"","sources":["../../../src/modules/repo/guards.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,MAAM,EAAE,sBAAsB,EAAE,MAAM,4BAA4B,CAAC;AACjF,OAAO,EAKL,KAAK,eAAe,EACpB,KAAK,kBAAkB,EACvB,KAAK,sBAAsB,EAC3B,KAAK,kBAAkB,EACxB,MAAM,iBAAiB,CAAC;AACzB,OAAO,EAAiB,KAAK,aAAa,EAAE,MAAM,gBAAgB,CAAC;AACnE,OAAO,EAA2B,KAAK,uBAAuB,EAAE,MAAM,2BAA2B,CAAC;AAClG,OAAO,EAAsB,KAAK,kBAAkB,EAAE,MAAM,qBAAqB,CAAC;AAClF,OAAO,EAAwB,KAAK,oBAAoB,EAAE,MAAM,wBAAwB,CAAC;AACzF,OAAO,EAEL,KAAK,8BAA8B,EACpC,MAAM,kCAAkC,CAAC;AAC1C,OAAO,EAA2B,KAAK,uBAAuB,EAAE,MAAM,2BAA2B,CAAC;AAClG,OAAO,EAAsB,KAAK,kBAAkB,EAAE,MAAM,sBAAsB,CAAC;AACnF,OAAO,EAAsB,KAAK,kBAAkB,EAAE,MAAM,sBAAsB,CAAC;AACnF,OAAO,EAAkB,KAAK,cAAc,EAAE,MAAM,iBAAiB,CAAC;AACtE,OAAO,EAA2B,KAAK,uBAAuB,EAAE,MAAM,6BAA6B,CAAC;AACpG,OAAO,EAAuB,KAAK,mBAAmB,EAAE,MAAM,uBAAuB,CAAC;AACtF,OAAO,EAA0B,KAAK,sBAAsB,EAAE,MAAM,0BAA0B,CAAC;AAC/F,OAAO,EAA0B,KAAK,sBAAsB,EAAE,MAAM,0BAA0B,CAAC;AAC/F,OAAO,EAAqB,KAAK,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AAE/E,0DAA0D;AAC1D,MAAM,WAAW,cAAc;IAC7B,uBAAuB,EAAE,cAAc,CAAC;IACxC,iBAAiB,EAAE,sBAAsB,CAAC;IAC1C,cAAc,EAAE,mBAAmB,GAAG,MAAM,CAAC;IAC7C,mBAAmB,EAAE,uBAAuB,CAAC;IAC7C,kBAAkB,EAAE,uBAAuB,CAAC;IAC5C,qBAAqB,EAAE,iBAAiB,GAAG,MAAM,CAAC;IAClD,iBAAiB,EAAE,sBAAsB,GAAG,MAAM,CAAC;IACnD,aAAa,EAAE,kBAAkB,CAAC;IAClC,qBAAqB,EAAE,kBAAkB,CAAC;IAC1C,kBAAkB,EAAE,eAAe,CAAC;IACpC,iBAAiB,EAAE,sBAAsB,CAAC;IAC1C,cAAc,EAAE,kBAAkB,GAAG,MAAM,CAAC;IAC5C,cAAc,EAAE,kBAAkB,GAAG,MAAM,CAAC;IAC5C,aAAa,EAAE,kBAAkB,CAAC;IAClC,eAAe,EAAE,oBAAoB,GAAG,MAAM,CAAC;IAC/C,yBAAyB,EAAE,8BAA8B,CAAC;IAC1D,QAAQ,EAAE,aAAa,GAAG,MAAM,CAAC;IACjC,mBAAmB,EAAE,uBAAuB,GAAG,MAAM,CAAC;CACvD;AAED,eAAO,MAAM,WAAW,EAAE,aAAa,CAAC,sBAAsB,CAAC,KAAK,CAAC,CAmBnB,CAAC"}
|
|
@@ -10,6 +10,7 @@ import { localOnlyVariablesGuard } from './local-only-variables.js';
|
|
|
10
10
|
import { projectScriptGuard } from './project-scripts.js';
|
|
11
11
|
import { secretBindingGuard } from './secret-bindings.js';
|
|
12
12
|
import { shellSafeGuard } from './shell-safe.js';
|
|
13
|
+
import { testCaseModuleLoadGuard } from './test-case-module-loads.js';
|
|
13
14
|
import { testSourceRootGuard } from './test-source-root.js';
|
|
14
15
|
import { walkerReadFailureGuard } from './walker-read-failure.js';
|
|
15
16
|
import { workerBoundedBodyGuard } from './worker-bounded-body.js';
|
|
@@ -18,6 +19,7 @@ export const REPO_GUARDS = Object.freeze([
|
|
|
18
19
|
shellSafeGuard,
|
|
19
20
|
walkerReadFailureGuard,
|
|
20
21
|
testSourceRootGuard,
|
|
22
|
+
testCaseModuleLoadGuard,
|
|
21
23
|
localOnlyVariablesGuard,
|
|
22
24
|
workerConfigGuard,
|
|
23
25
|
workerBoundedBodyGuard,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"guards.js","sourceRoot":"","sources":["../../../src/modules/repo/guards.ts"],"names":[],"mappings":"AAAA,gJAAgJ;AAChJ,iCAAiC;AAGjC,OAAO,EACL,eAAe,EACf,kBAAkB,EAClB,sBAAsB,EACtB,kBAAkB,GAKnB,MAAM,iBAAiB,CAAC;AACzB,OAAO,EAAE,aAAa,EAAsB,MAAM,gBAAgB,CAAC;AACnE,OAAO,EAAE,uBAAuB,EAAgC,MAAM,2BAA2B,CAAC;AAClG,OAAO,EAAE,kBAAkB,EAA2B,MAAM,qBAAqB,CAAC;AAClF,OAAO,EAAE,oBAAoB,EAA6B,MAAM,wBAAwB,CAAC;AACzF,OAAO,EACL,8BAA8B,GAE/B,MAAM,kCAAkC,CAAC;AAC1C,OAAO,EAAE,uBAAuB,EAAgC,MAAM,2BAA2B,CAAC;AAClG,OAAO,EAAE,kBAAkB,EAA2B,MAAM,sBAAsB,CAAC;AACnF,OAAO,EAAE,kBAAkB,EAA2B,MAAM,sBAAsB,CAAC;AACnF,OAAO,EAAE,cAAc,EAAuB,MAAM,iBAAiB,CAAC;AACtE,OAAO,EAAE,mBAAmB,EAA4B,MAAM,uBAAuB,CAAC;AACtF,OAAO,EAAE,sBAAsB,EAA+B,MAAM,0BAA0B,CAAC;AAC/F,OAAO,EAAE,sBAAsB,EAA+B,MAAM,0BAA0B,CAAC;AAC/F,OAAO,EAAE,iBAAiB,EAA0B,MAAM,oBAAoB,CAAC;
|
|
1
|
+
{"version":3,"file":"guards.js","sourceRoot":"","sources":["../../../src/modules/repo/guards.ts"],"names":[],"mappings":"AAAA,gJAAgJ;AAChJ,iCAAiC;AAGjC,OAAO,EACL,eAAe,EACf,kBAAkB,EAClB,sBAAsB,EACtB,kBAAkB,GAKnB,MAAM,iBAAiB,CAAC;AACzB,OAAO,EAAE,aAAa,EAAsB,MAAM,gBAAgB,CAAC;AACnE,OAAO,EAAE,uBAAuB,EAAgC,MAAM,2BAA2B,CAAC;AAClG,OAAO,EAAE,kBAAkB,EAA2B,MAAM,qBAAqB,CAAC;AAClF,OAAO,EAAE,oBAAoB,EAA6B,MAAM,wBAAwB,CAAC;AACzF,OAAO,EACL,8BAA8B,GAE/B,MAAM,kCAAkC,CAAC;AAC1C,OAAO,EAAE,uBAAuB,EAAgC,MAAM,2BAA2B,CAAC;AAClG,OAAO,EAAE,kBAAkB,EAA2B,MAAM,sBAAsB,CAAC;AACnF,OAAO,EAAE,kBAAkB,EAA2B,MAAM,sBAAsB,CAAC;AACnF,OAAO,EAAE,cAAc,EAAuB,MAAM,iBAAiB,CAAC;AACtE,OAAO,EAAE,uBAAuB,EAAgC,MAAM,6BAA6B,CAAC;AACpG,OAAO,EAAE,mBAAmB,EAA4B,MAAM,uBAAuB,CAAC;AACtF,OAAO,EAAE,sBAAsB,EAA+B,MAAM,0BAA0B,CAAC;AAC/F,OAAO,EAAE,sBAAsB,EAA+B,MAAM,0BAA0B,CAAC;AAC/F,OAAO,EAAE,iBAAiB,EAA0B,MAAM,oBAAoB,CAAC;AAwB/E,MAAM,CAAC,MAAM,WAAW,GAAiD,MAAM,CAAC,MAAM,CAAC;IACrF,cAAc;IACd,sBAAsB;IACtB,mBAAmB;IACnB,uBAAuB;IACvB,uBAAuB;IACvB,iBAAiB;IACjB,sBAAsB;IACtB,kBAAkB;IAClB,kBAAkB;IAClB,eAAe;IACf,sBAAsB;IACtB,kBAAkB;IAClB,kBAAkB;IAClB,kBAAkB;IAClB,oBAAoB;IACpB,8BAA8B;IAC9B,aAAa;IACb,uBAAuB;CACwB,CAAC,CAAC","sourcesContent":["// The generic guards over the repository, its supply chain, and its tooling surface. The registry runs every guard listed here; an app supplies\n// each one's facts under its id.\n\nimport type { Absent, GenericGuardDefinition } from '../../node/suite/config.js';\nimport {\n entryPointGuard,\n isolatedRootsGuard,\n noOutsideCheckoutGuard,\n tsconfigAliasGuard,\n type EntryPointFacts,\n type IsolatedRootsFacts,\n type NoOutsideCheckoutFacts,\n type TsconfigAliasFacts,\n} from './cross-root.js';\nimport { ciParityGuard, type CiParityFacts } from './ci-parity.js';\nimport { dependencyOverrideGuard, type DependencyOverrideFacts } from './dependency-overrides.js';\nimport { documentDriftGuard, type DocumentDriftFacts } from './document-drift.js';\nimport { envPublicValuesGuard, type EnvPublicValuesFacts } from './env-public-values.js';\nimport {\n installedPackagesAgnosticGuard,\n type InstalledPackagesAgnosticFacts,\n} from './installed-packages-agnostic.js';\nimport { localOnlyVariablesGuard, type LocalOnlyVariablesFacts } from './local-only-variables.js';\nimport { projectScriptGuard, type ProjectScriptFacts } from './project-scripts.js';\nimport { secretBindingGuard, type SecretBindingFacts } from './secret-bindings.js';\nimport { shellSafeGuard, type ShellSafeFacts } from './shell-safe.js';\nimport { testCaseModuleLoadGuard, type TestCaseModuleLoadFacts } from './test-case-module-loads.js';\nimport { testSourceRootGuard, type TestSourceRootFacts } from './test-source-root.js';\nimport { walkerReadFailureGuard, type WalkerReadFailureFacts } from './walker-read-failure.js';\nimport { workerBoundedBodyGuard, type WorkerBoundedBodyFacts } from './worker-bounded-body.js';\nimport { workerConfigGuard, type WorkerConfigFacts } from './worker-config.js';\n\n/** The facts each guard in this area needs, by its id. */\nexport interface RepoGuardFacts {\n shellSafeChildProcesses: ShellSafeFacts;\n walkerReadFailure: WalkerReadFailureFacts;\n testSourceRoot: TestSourceRootFacts | Absent;\n testCaseModuleLoads: TestCaseModuleLoadFacts;\n localOnlyVariables: LocalOnlyVariablesFacts;\n workerConfigIsolation: WorkerConfigFacts | Absent;\n workerBoundedBody: WorkerBoundedBodyFacts | Absent;\n isolatedRoots: IsolatedRootsFacts;\n tsconfigAliasesInRoot: TsconfigAliasFacts;\n packageEntryPoints: EntryPointFacts;\n noOutsideCheckout: NoOutsideCheckoutFacts;\n projectScripts: ProjectScriptFacts | Absent;\n secretBindings: SecretBindingFacts | Absent;\n documentDrift: DocumentDriftFacts;\n envPublicValues: EnvPublicValuesFacts | Absent;\n installedPackagesAgnostic: InstalledPackagesAgnosticFacts;\n ciParity: CiParityFacts | Absent;\n dependencyOverrides: DependencyOverrideFacts | Absent;\n}\n\nexport const REPO_GUARDS: ReadonlyArray<GenericGuardDefinition<never>> = Object.freeze([\n shellSafeGuard,\n walkerReadFailureGuard,\n testSourceRootGuard,\n testCaseModuleLoadGuard,\n localOnlyVariablesGuard,\n workerConfigGuard,\n workerBoundedBodyGuard,\n isolatedRootsGuard,\n tsconfigAliasGuard,\n entryPointGuard,\n noOutsideCheckoutGuard,\n projectScriptGuard,\n secretBindingGuard,\n documentDriftGuard,\n envPublicValuesGuard,\n installedPackagesAgnosticGuard,\n ciParityGuard,\n dependencyOverrideGuard,\n] as ReadonlyArray<GenericGuardDefinition<never>>);\n"]}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { GenericGuardDefinition } from '../../node/suite/config.js';
|
|
2
|
+
import { type FileView } from '../../reader/view.js';
|
|
3
|
+
export interface TestCaseModuleLoad {
|
|
4
|
+
index: number;
|
|
5
|
+
detail: string;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Every place a test file loads code inside a case: a module-registry reset, or a dynamic `import()`
|
|
9
|
+
* of a local module run from a case, a hook, or a helper. A string is read as a sample test file.
|
|
10
|
+
*/
|
|
11
|
+
export declare function testCaseModuleLoads(source: FileView | string, aliases: readonly string[]): TestCaseModuleLoad[];
|
|
12
|
+
export interface TestCaseModuleLoadFacts {
|
|
13
|
+
/**
|
|
14
|
+
* The prefixes the app's own import aliases begin with (`@/`), beside relative paths; `[]` when
|
|
15
|
+
* the app has none.
|
|
16
|
+
*/
|
|
17
|
+
localAliases: readonly string[];
|
|
18
|
+
/**
|
|
19
|
+
* The test files that must load a module fresh per case, each with its why (code that reads its
|
|
20
|
+
* environment once at load, tested under several environments). One that no longer does is stale.
|
|
21
|
+
*/
|
|
22
|
+
reviewed: ReadonlyArray<{
|
|
23
|
+
path: string;
|
|
24
|
+
why: string;
|
|
25
|
+
}>;
|
|
26
|
+
}
|
|
27
|
+
export declare const testCaseModuleLoadGuard: GenericGuardDefinition<TestCaseModuleLoadFacts>;
|
|
28
|
+
//# sourceMappingURL=test-case-module-loads.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"test-case-module-loads.d.ts","sourceRoot":"","sources":["../../../src/modules/repo/test-case-module-loads.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,4BAA4B,CAAC;AAGzE,OAAO,EAAc,KAAK,QAAQ,EAAE,MAAM,sBAAsB,CAAC;AAwCjE,MAAM,WAAW,kBAAkB;IACjC,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;CAChB;AAED;;;GAGG;AACH,wBAAgB,mBAAmB,CACjC,MAAM,EAAE,QAAQ,GAAG,MAAM,EACzB,OAAO,EAAE,SAAS,MAAM,EAAE,GACzB,kBAAkB,EAAE,CAoBtB;AASD,MAAM,WAAW,uBAAuB;IACtC;;;OAGG;IACH,YAAY,EAAE,SAAS,MAAM,EAAE,CAAC;IAChC;;;OAGG;IACH,QAAQ,EAAE,aAAa,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,GAAG,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;CACxD;AAED,eAAO,MAAM,uBAAuB,EAAE,sBAAsB,CAAC,uBAAuB,CAmDnF,CAAC"}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
// A test loads the code it tests once, when its file loads — never inside a test case. A dynamic
|
|
2
|
+
// `import()` of a local module run from a test, a hook, or a helper they call pays the module
|
|
3
|
+
// graph's cold load inside that case's timeout, so the first case of a file is slow for no work of
|
|
4
|
+
// its own and, under a full parallel run, crosses the timeout; a module-registry reset re-pays that
|
|
5
|
+
// load for every case. A static import (or a top-level `await import()`) loads it at collection,
|
|
6
|
+
// where no case is timed. A file that truly needs a fresh module per case — code that reads its
|
|
7
|
+
// environment once at load and is tested under several environments — is a reviewed entry with its
|
|
8
|
+
// why; one that no longer does either is stale.
|
|
9
|
+
import ts from 'typescript';
|
|
10
|
+
import { viewOfText } from '../../reader/view.js';
|
|
11
|
+
import { forEachNode } from '../../support/ast.js';
|
|
12
|
+
import { namesProblem, objectProblem, reviewedProblems } from '../../support/facts.js';
|
|
13
|
+
/** The test-runner calls that empty the module registry, so the next import loads every module anew. */
|
|
14
|
+
const REGISTRY_RESETS = new Set(['vi.resetModules', 'jest.resetModules']);
|
|
15
|
+
/** Calls whose callback runs while the file is collected, never inside a timed case. */
|
|
16
|
+
const COLLECTION_CALLBACKS = new Set([
|
|
17
|
+
'vi.hoisted',
|
|
18
|
+
'vi.mock',
|
|
19
|
+
'vi.doMock',
|
|
20
|
+
'jest.mock',
|
|
21
|
+
'jest.doMock',
|
|
22
|
+
]);
|
|
23
|
+
const dottedCallee = (call) => ts.isPropertyAccessExpression(call.expression) && ts.isIdentifier(call.expression.expression)
|
|
24
|
+
? `${call.expression.expression.text}.${call.expression.name.text}`
|
|
25
|
+
: null;
|
|
26
|
+
/** A specifier naming the app's own code: a relative path, or one of the app's import aliases. */
|
|
27
|
+
const isLocal = (spec, aliases) => spec.startsWith('./') || spec.startsWith('../') || aliases.some((a) => spec.startsWith(a));
|
|
28
|
+
/**
|
|
29
|
+
* Whether code at `node` runs inside a function a test runs — a case, a hook, or a helper — rather
|
|
30
|
+
* than at collection: inside any function that is not the callback of a collection-time call.
|
|
31
|
+
*/
|
|
32
|
+
function runsInsideACase(node) {
|
|
33
|
+
for (let at = node.parent; at; at = at.parent) {
|
|
34
|
+
if (!ts.isFunctionLike(at))
|
|
35
|
+
continue;
|
|
36
|
+
const call = at.parent;
|
|
37
|
+
if (call && ts.isCallExpression(call) && COLLECTION_CALLBACKS.has(dottedCallee(call) ?? ''))
|
|
38
|
+
return false;
|
|
39
|
+
return true;
|
|
40
|
+
}
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Every place a test file loads code inside a case: a module-registry reset, or a dynamic `import()`
|
|
45
|
+
* of a local module run from a case, a hook, or a helper. A string is read as a sample test file.
|
|
46
|
+
*/
|
|
47
|
+
export function testCaseModuleLoads(source, aliases) {
|
|
48
|
+
const file = typeof source === 'string' ? viewOfText('sample.test.ts', source) : source;
|
|
49
|
+
const sf = file.sourceFile;
|
|
50
|
+
const hits = [];
|
|
51
|
+
forEachNode(sf, (node) => {
|
|
52
|
+
if (ts.isCallExpression(node) && REGISTRY_RESETS.has(dottedCallee(node) ?? ''))
|
|
53
|
+
hits.push({
|
|
54
|
+
index: node.getStart(sf),
|
|
55
|
+
detail: `${dottedCallee(node)}() reloads every module for the cases after it`,
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
for (const load of file.imports) {
|
|
59
|
+
if (load.kind !== 'dynamic' || load.typeOnly || !isLocal(load.spec, aliases))
|
|
60
|
+
continue;
|
|
61
|
+
if (!runsInsideACase(load.node))
|
|
62
|
+
continue;
|
|
63
|
+
hits.push({
|
|
64
|
+
index: load.node.getStart(sf),
|
|
65
|
+
detail: `import('${load.spec}') loads the module inside a test case`,
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
return hits.sort((a, b) => a.index - b.index);
|
|
69
|
+
}
|
|
70
|
+
const SCOPE = {
|
|
71
|
+
roots: ['app', 'functions', 'scripts', 'workers'],
|
|
72
|
+
kinds: ['code'],
|
|
73
|
+
tests: 'only',
|
|
74
|
+
optionalRoots: ['functions', 'scripts', 'workers'],
|
|
75
|
+
};
|
|
76
|
+
export const testCaseModuleLoadGuard = {
|
|
77
|
+
id: 'testCaseModuleLoads',
|
|
78
|
+
absentWhen: false,
|
|
79
|
+
factProblems(facts) {
|
|
80
|
+
const problems = [];
|
|
81
|
+
if (!objectProblem(facts, 'the facts', problems))
|
|
82
|
+
return problems;
|
|
83
|
+
namesProblem(facts.localAliases, 'localAliases', problems, { nonEmpty: false });
|
|
84
|
+
reviewedProblems(facts.reviewed, 'reviewed', problems);
|
|
85
|
+
return problems;
|
|
86
|
+
},
|
|
87
|
+
build(facts) {
|
|
88
|
+
const reviewed = new Map(facts.reviewed.map((entry) => [entry.path, entry]));
|
|
89
|
+
return [
|
|
90
|
+
{
|
|
91
|
+
title: 'a test loads the code it tests when its file loads, never inside a test case',
|
|
92
|
+
description: "No test, hook, or helper a test calls resets the module registry or runs a dynamic import() of the app's own " +
|
|
93
|
+
"code: the module graph's cold load then lands inside one case's timeout — the file's first case is slow for no " +
|
|
94
|
+
'work of its own, and under a full parallel run it times out. Import the code at the top of the file (set any ' +
|
|
95
|
+
'environment it reads at load in vi.hoisted first). A file that must load a module fresh per case is a reviewed ' +
|
|
96
|
+
'entry with its why; an entry whose file no longer does is stale.',
|
|
97
|
+
timeoutMs: 120_000,
|
|
98
|
+
run(context) {
|
|
99
|
+
const findings = [];
|
|
100
|
+
const seen = new Set();
|
|
101
|
+
for (const file of context.files(SCOPE)) {
|
|
102
|
+
const loads = testCaseModuleLoads(file, facts.localAliases);
|
|
103
|
+
if (!loads.length)
|
|
104
|
+
continue;
|
|
105
|
+
if (reviewed.has(file.path)) {
|
|
106
|
+
seen.add(file.path);
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
for (const load of loads)
|
|
110
|
+
findings.push({
|
|
111
|
+
file: file.path,
|
|
112
|
+
line: file.lineOf(load.index),
|
|
113
|
+
detail: load.detail,
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
for (const path of reviewed.keys())
|
|
117
|
+
if (!seen.has(path))
|
|
118
|
+
findings.push({
|
|
119
|
+
file: path,
|
|
120
|
+
detail: 'stale reviewed entry: the file no longer loads a module inside a test case',
|
|
121
|
+
});
|
|
122
|
+
return findings;
|
|
123
|
+
},
|
|
124
|
+
},
|
|
125
|
+
];
|
|
126
|
+
},
|
|
127
|
+
};
|
|
128
|
+
//# sourceMappingURL=test-case-module-loads.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"test-case-module-loads.js","sourceRoot":"","sources":["../../../src/modules/repo/test-case-module-loads.ts"],"names":[],"mappings":"AAAA,iGAAiG;AACjG,8FAA8F;AAC9F,mGAAmG;AACnG,oGAAoG;AACpG,iGAAiG;AACjG,gGAAgG;AAChG,mGAAmG;AACnG,gDAAgD;AAEhD,OAAO,EAAE,MAAM,YAAY,CAAC;AAI5B,OAAO,EAAE,UAAU,EAAiB,MAAM,sBAAsB,CAAC;AACjE,OAAO,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAC;AACnD,OAAO,EAAE,YAAY,EAAE,aAAa,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAEvF,wGAAwG;AACxG,MAAM,eAAe,GAAwB,IAAI,GAAG,CAAC,CAAC,iBAAiB,EAAE,mBAAmB,CAAC,CAAC,CAAC;AAE/F,wFAAwF;AACxF,MAAM,oBAAoB,GAAwB,IAAI,GAAG,CAAC;IACxD,YAAY;IACZ,SAAS;IACT,WAAW;IACX,WAAW;IACX,aAAa;CACd,CAAC,CAAC;AAEH,MAAM,YAAY,GAAG,CAAC,IAAuB,EAAiB,EAAE,CAC9D,EAAE,CAAC,0BAA0B,CAAC,IAAI,CAAC,UAAU,CAAC,IAAI,EAAE,CAAC,YAAY,CAAC,IAAI,CAAC,UAAU,CAAC,UAAU,CAAC;IAC3F,CAAC,CAAC,GAAG,IAAI,CAAC,UAAU,CAAC,UAAU,CAAC,IAAI,IAAI,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC,IAAI,EAAE;IACnE,CAAC,CAAC,IAAI,CAAC;AAEX,kGAAkG;AAClG,MAAM,OAAO,GAAG,CAAC,IAAY,EAAE,OAA0B,EAAE,EAAE,CAC3D,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC,IAAI,IAAI,CAAC,UAAU,CAAC,KAAK,CAAC,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,CAAC;AAE7F;;;GAGG;AACH,SAAS,eAAe,CAAC,IAAa;IACpC,KAAK,IAAI,EAAE,GAAwB,IAAI,CAAC,MAAM,EAAE,EAAE,EAAE,EAAE,GAAG,EAAE,CAAC,MAAM,EAAE,CAAC;QACnE,IAAI,CAAC,EAAE,CAAC,cAAc,CAAC,EAAE,CAAC;YAAE,SAAS;QACrC,MAAM,IAAI,GAAG,EAAE,CAAC,MAAM,CAAC;QACvB,IAAI,IAAI,IAAI,EAAE,CAAC,gBAAgB,CAAC,IAAI,CAAC,IAAI,oBAAoB,CAAC,GAAG,CAAC,YAAY,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC;YACzF,OAAO,KAAK,CAAC;QACf,OAAO,IAAI,CAAC;IACd,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC;AAOD;;;GAGG;AACH,MAAM,UAAU,mBAAmB,CACjC,MAAyB,EACzB,OAA0B;IAE1B,MAAM,IAAI,GAAG,OAAO,MAAM,KAAK,QAAQ,CAAC,CAAC,CAAC,UAAU,CAAC,gBAAgB,EAAE,MAAM,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC;IACxF,MAAM,EAAE,GAAG,IAAI,CAAC,UAAU,CAAC;IAC3B,MAAM,IAAI,GAAyB,EAAE,CAAC;IACtC,WAAW,CAAC,EAAE,EAAE,CAAC,IAAI,EAAE,EAAE;QACvB,IAAI,EAAE,CAAC,gBAAgB,CAAC,IAAI,CAAC,IAAI,eAAe,CAAC,GAAG,CAAC,YAAY,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC;YAC5E,IAAI,CAAC,IAAI,CAAC;gBACR,KAAK,EAAE,IAAI,CAAC,QAAQ,CAAC,EAAE,CAAC;gBACxB,MAAM,EAAE,GAAG,YAAY,CAAC,IAAI,CAAC,gDAAgD;aAC9E,CAAC,CAAC;IACP,CAAC,CAAC,CAAC;IACH,KAAK,MAAM,IAAI,IAAI,IAAI,CAAC,OAAO,EAAE,CAAC;QAChC,IAAI,IAAI,CAAC,IAAI,KAAK,SAAS,IAAI,IAAI,CAAC,QAAQ,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,IAAI,EAAE,OAAO,CAAC;YAAE,SAAS;QACvF,IAAI,CAAC,eAAe,CAAC,IAAI,CAAC,IAAI,CAAC;YAAE,SAAS;QAC1C,IAAI,CAAC,IAAI,CAAC;YACR,KAAK,EAAE,IAAI,CAAC,IAAI,CAAC,QAAQ,CAAC,EAAE,CAAC;YAC7B,MAAM,EAAE,WAAW,IAAI,CAAC,IAAI,wCAAwC;SACrE,CAAC,CAAC;IACL,CAAC;IACD,OAAO,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,KAAK,GAAG,CAAC,CAAC,KAAK,CAAC,CAAC;AAChD,CAAC;AAED,MAAM,KAAK,GAAU;IACnB,KAAK,EAAE,CAAC,KAAK,EAAE,WAAW,EAAE,SAAS,EAAE,SAAS,CAAC;IACjD,KAAK,EAAE,CAAC,MAAM,CAAC;IACf,KAAK,EAAE,MAAM;IACb,aAAa,EAAE,CAAC,WAAW,EAAE,SAAS,EAAE,SAAS,CAAC;CACnD,CAAC;AAeF,MAAM,CAAC,MAAM,uBAAuB,GAAoD;IACtF,EAAE,EAAE,qBAAqB;IACzB,UAAU,EAAE,KAAK;IACjB,YAAY,CAAC,KAAK;QAChB,MAAM,QAAQ,GAAa,EAAE,CAAC;QAC9B,IAAI,CAAC,aAAa,CAAC,KAAK,EAAE,WAAW,EAAE,QAAQ,CAAC;YAAE,OAAO,QAAQ,CAAC;QAClE,YAAY,CAAC,KAAK,CAAC,YAAY,EAAE,cAAc,EAAE,QAAQ,EAAE,EAAE,QAAQ,EAAE,KAAK,EAAE,CAAC,CAAC;QAChF,gBAAgB,CAAC,KAAK,CAAC,QAAQ,EAAE,UAAU,EAAE,QAAQ,CAAC,CAAC;QACvD,OAAO,QAAQ,CAAC;IAClB,CAAC;IACD,KAAK,CAAC,KAAK;QACT,MAAM,QAAQ,GAAG,IAAI,GAAG,CAAC,KAAK,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC,KAAK,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC,CAAC,CAAC;QAC7E,OAAO;YACL;gBACE,KAAK,EAAE,8EAA8E;gBACrF,WAAW,EACT,+GAA+G;oBAC/G,iHAAiH;oBACjH,+GAA+G;oBAC/G,iHAAiH;oBACjH,kEAAkE;gBACpE,SAAS,EAAE,OAAO;gBAClB,GAAG,CAAC,OAAO;oBACT,MAAM,QAAQ,GAAc,EAAE,CAAC;oBAC/B,MAAM,IAAI,GAAG,IAAI,GAAG,EAAU,CAAC;oBAC/B,KAAK,MAAM,IAAI,IAAI,OAAO,CAAC,KAAK,CAAC,KAAK,CAAC,EAAE,CAAC;wBACxC,MAAM,KAAK,GAAG,mBAAmB,CAAC,IAAI,EAAE,KAAK,CAAC,YAAY,CAAC,CAAC;wBAC5D,IAAI,CAAC,KAAK,CAAC,MAAM;4BAAE,SAAS;wBAC5B,IAAI,QAAQ,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC;4BAC5B,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;4BACpB,SAAS;wBACX,CAAC;wBACD,KAAK,MAAM,IAAI,IAAI,KAAK;4BACtB,QAAQ,CAAC,IAAI,CAAC;gCACZ,IAAI,EAAE,IAAI,CAAC,IAAI;gCACf,IAAI,EAAE,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC;gCAC7B,MAAM,EAAE,IAAI,CAAC,MAAM;6BACpB,CAAC,CAAC;oBACP,CAAC;oBACD,KAAK,MAAM,IAAI,IAAI,QAAQ,CAAC,IAAI,EAAE;wBAChC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC;4BACjB,QAAQ,CAAC,IAAI,CAAC;gCACZ,IAAI,EAAE,IAAI;gCACV,MAAM,EACJ,4EAA4E;6BAC/E,CAAC,CAAC;oBACP,OAAO,QAAQ,CAAC;gBAClB,CAAC;aACF;SACF,CAAC;IACJ,CAAC;CACF,CAAC","sourcesContent":["// A test loads the code it tests once, when its file loads — never inside a test case. A dynamic\n// `import()` of a local module run from a test, a hook, or a helper they call pays the module\n// graph's cold load inside that case's timeout, so the first case of a file is slow for no work of\n// its own and, under a full parallel run, crosses the timeout; a module-registry reset re-pays that\n// load for every case. A static import (or a top-level `await import()`) loads it at collection,\n// where no case is timed. A file that truly needs a fresh module per case — code that reads its\n// environment once at load and is tested under several environments — is a reviewed entry with its\n// why; one that no longer does either is stale.\n\nimport ts from 'typescript';\nimport type { GenericGuardDefinition } from '../../node/suite/config.js';\nimport type { Finding } from '../../guard.js';\nimport type { Scope } from '../../node/reader.js';\nimport { viewOfText, type FileView } from '../../reader/view.js';\nimport { forEachNode } from '../../support/ast.js';\nimport { namesProblem, objectProblem, reviewedProblems } from '../../support/facts.js';\n\n/** The test-runner calls that empty the module registry, so the next import loads every module anew. */\nconst REGISTRY_RESETS: ReadonlySet<string> = new Set(['vi.resetModules', 'jest.resetModules']);\n\n/** Calls whose callback runs while the file is collected, never inside a timed case. */\nconst COLLECTION_CALLBACKS: ReadonlySet<string> = new Set([\n 'vi.hoisted',\n 'vi.mock',\n 'vi.doMock',\n 'jest.mock',\n 'jest.doMock',\n]);\n\nconst dottedCallee = (call: ts.CallExpression): string | null =>\n ts.isPropertyAccessExpression(call.expression) && ts.isIdentifier(call.expression.expression)\n ? `${call.expression.expression.text}.${call.expression.name.text}`\n : null;\n\n/** A specifier naming the app's own code: a relative path, or one of the app's import aliases. */\nconst isLocal = (spec: string, aliases: readonly string[]) =>\n spec.startsWith('./') || spec.startsWith('../') || aliases.some((a) => spec.startsWith(a));\n\n/**\n * Whether code at `node` runs inside a function a test runs — a case, a hook, or a helper — rather\n * than at collection: inside any function that is not the callback of a collection-time call.\n */\nfunction runsInsideACase(node: ts.Node): boolean {\n for (let at: ts.Node | undefined = node.parent; at; at = at.parent) {\n if (!ts.isFunctionLike(at)) continue;\n const call = at.parent;\n if (call && ts.isCallExpression(call) && COLLECTION_CALLBACKS.has(dottedCallee(call) ?? ''))\n return false;\n return true;\n }\n return false;\n}\n\nexport interface TestCaseModuleLoad {\n index: number;\n detail: string;\n}\n\n/**\n * Every place a test file loads code inside a case: a module-registry reset, or a dynamic `import()`\n * of a local module run from a case, a hook, or a helper. A string is read as a sample test file.\n */\nexport function testCaseModuleLoads(\n source: FileView | string,\n aliases: readonly string[],\n): TestCaseModuleLoad[] {\n const file = typeof source === 'string' ? viewOfText('sample.test.ts', source) : source;\n const sf = file.sourceFile;\n const hits: TestCaseModuleLoad[] = [];\n forEachNode(sf, (node) => {\n if (ts.isCallExpression(node) && REGISTRY_RESETS.has(dottedCallee(node) ?? ''))\n hits.push({\n index: node.getStart(sf),\n detail: `${dottedCallee(node)}() reloads every module for the cases after it`,\n });\n });\n for (const load of file.imports) {\n if (load.kind !== 'dynamic' || load.typeOnly || !isLocal(load.spec, aliases)) continue;\n if (!runsInsideACase(load.node)) continue;\n hits.push({\n index: load.node.getStart(sf),\n detail: `import('${load.spec}') loads the module inside a test case`,\n });\n }\n return hits.sort((a, b) => a.index - b.index);\n}\n\nconst SCOPE: Scope = {\n roots: ['app', 'functions', 'scripts', 'workers'],\n kinds: ['code'],\n tests: 'only',\n optionalRoots: ['functions', 'scripts', 'workers'],\n};\n\nexport interface TestCaseModuleLoadFacts {\n /**\n * The prefixes the app's own import aliases begin with (`@/`), beside relative paths; `[]` when\n * the app has none.\n */\n localAliases: readonly string[];\n /**\n * The test files that must load a module fresh per case, each with its why (code that reads its\n * environment once at load, tested under several environments). One that no longer does is stale.\n */\n reviewed: ReadonlyArray<{ path: string; why: string }>;\n}\n\nexport const testCaseModuleLoadGuard: GenericGuardDefinition<TestCaseModuleLoadFacts> = {\n id: 'testCaseModuleLoads',\n absentWhen: false,\n factProblems(facts) {\n const problems: string[] = [];\n if (!objectProblem(facts, 'the facts', problems)) return problems;\n namesProblem(facts.localAliases, 'localAliases', problems, { nonEmpty: false });\n reviewedProblems(facts.reviewed, 'reviewed', problems);\n return problems;\n },\n build(facts) {\n const reviewed = new Map(facts.reviewed.map((entry) => [entry.path, entry]));\n return [\n {\n title: 'a test loads the code it tests when its file loads, never inside a test case',\n description:\n \"No test, hook, or helper a test calls resets the module registry or runs a dynamic import() of the app's own \" +\n \"code: the module graph's cold load then lands inside one case's timeout — the file's first case is slow for no \" +\n 'work of its own, and under a full parallel run it times out. Import the code at the top of the file (set any ' +\n 'environment it reads at load in vi.hoisted first). A file that must load a module fresh per case is a reviewed ' +\n 'entry with its why; an entry whose file no longer does is stale.',\n timeoutMs: 120_000,\n run(context) {\n const findings: Finding[] = [];\n const seen = new Set<string>();\n for (const file of context.files(SCOPE)) {\n const loads = testCaseModuleLoads(file, facts.localAliases);\n if (!loads.length) continue;\n if (reviewed.has(file.path)) {\n seen.add(file.path);\n continue;\n }\n for (const load of loads)\n findings.push({\n file: file.path,\n line: file.lineOf(load.index),\n detail: load.detail,\n });\n }\n for (const path of reviewed.keys())\n if (!seen.has(path))\n findings.push({\n file: path,\n detail:\n 'stale reviewed entry: the file no longer loads a module inside a test case',\n });\n return findings;\n },\n },\n ];\n },\n};\n"]}
|