workspace-conformance 1.3.0 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +12 -1
- package/dist/cli.js +158 -13
- package/dist/index.cjs +159 -12
- package/dist/index.d.cts +48 -1
- package/dist/index.d.ts +48 -1
- package/dist/index.js +159 -14
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -128,6 +128,17 @@ Codes: `aggregate-mappers/missing-mapper` (reported at the aggregate's declarati
|
|
|
128
128
|
|
|
129
129
|
Codes: `command-types/hand-written-interface`, `command-types/hand-written-alias` (an alias that applies none of the generics, including a union or a plain object type) and `command-types/not-a-schema` (an inference applied to something whose type has no `~standard` member). Limits: a union of commands is a hand-written alias unless excluded; a type exported from several listed files is judged once; only interfaces and type aliases are judged, including those inside an exported namespace or a module re-exported as a namespace (under their qualified names), so a class, enum or value is not a command here; when the schema library is not installed the schema's type does not resolve and the alias reports `not-a-schema`.
|
|
130
130
|
|
|
131
|
+
**`derived-types`** verifies that the types a file exports are derived from the schemas they belong with, for each configured pair of schema files and type files. It covers what `command-types` does not: a type written by hand next to the schema it should be inferred from, a table row type that should come from the table definition, an error type that should come from its error schema. An exported type is derived when somewhere in its definition a generic named in `inferences` is applied to `typeof schema` (`z.infer<typeof order>`, `InferSelectModel<typeof users>`), or a member named there is read from it (`typeof users.$inferSelect`), where `schema` is declared in one of the pair's schema files, through any imports and re-exports. The definition is followed through aliases and interfaces without type parameters, in any file, so an alias chain across files counts when it ends in an inference; through the type arguments of other generics (`Partial<Order>`); through an intersection with at least one derived member, a union whose members are all derived (`null` and `undefined` aside), arrays, type operators, indexed access and the `extends` clauses of an interface. A type literal is hand-written whatever its members are, and so is a schema passed to a generic that is not an inference (`Wrapper<typeof order>`) or an inference of a schema declared outside the pair's schema files.
|
|
132
|
+
|
|
133
|
+
| Option | Meaning |
|
|
134
|
+
|---|---|
|
|
135
|
+
| `pairs` | A list of `{ schemas, types }`, each a list of globs: every exported interface and type alias in the `types` files must be derived from a schema declared in the `schemas` files. A type file listed by several pairs is judged against each. |
|
|
136
|
+
| `exclude` | Names of exported types that are deliberately written by hand. |
|
|
137
|
+
| `inferences` | Replaces the default list of last names that count as inference: those of `command-types` plus Drizzle's `InferSelectModel`, `InferInsertModel`, `$inferSelect` and `$inferInsert` (`DEFAULT_DERIVED_TYPE_INFERENCES`). |
|
|
138
|
+
| `tsConfig` | See above. |
|
|
139
|
+
|
|
140
|
+
Code: `derived-types/not-derived`, reported at the type's declaration. Limits: a schema is recognised by where it is declared, not by its type, so anything in a schema file passed to an inference counts; a tuple type is not followed and is hand-written; a type exported from several type files of one pair is judged once; types inside an exported namespace or a module re-exported as a namespace are judged under their qualified names, which `exclude` also matches; a glob that matches no file and a missing tsconfig are configuration errors naming the option (`checks.derived-types.pairs.0.schemas`).
|
|
141
|
+
|
|
131
142
|
### File tree and cross-config
|
|
132
143
|
|
|
133
144
|
**`instruction-symlinks`** verifies that agent instruction files are symbolic links to the README beside them (written in any form that resolves there, such as `./README.md`, or through other tracked links, as `CLAUDE.md` to `AGENTS.md` to `README.md`), as git records them: mode `120000` in the index, not the working tree, so a checkout with `core.symlinks` false, which writes links out as plain files, is judged by what is committed. Options: `files` (`AGENTS.md` and `CLAUDE.md`), `directories` (globs, `.`), `target` (`README.md`). Codes: `instruction-symlinks/missing` (not tracked), `not-a-symlink`, `wrong-target` (the link does not resolve to the README, including a loop or an absolute target), `dangling` (the target is not tracked). It needs a git working tree and reads the index, so a link that is staged counts before it is committed.
|
|
@@ -283,7 +294,7 @@ it('conforms to the workspace layout', async () => {
|
|
|
283
294
|
});
|
|
284
295
|
```
|
|
285
296
|
|
|
286
|
-
Every check is also exported as a function taking `{ cwd, options }` (and `layout` for the import checks): `aggregateMappers`, `commandTypes`, `importUphill`, `importRankSkip`, `importCrossSlice`, `importIsolatedGroups`, `importCycles`, `instructionSymlinks`, `singleStorybook`, `commitTypes`, `dockerfilePackageManager`, `migrationsDirectory`, `eslint`, the workflow checks (`workflowJobOrdering`, `workflowSkippableJobs`, `workflowRunnerResolution`, `workflowVersionSingleSource`, `workflowCredentials`, `workflowRepositoryDispatch`, `workflowUpdateBotCooldown`, `workflowMergeGroup` and `workflowActionPinning`) and, taking `{ cwd, options, github }`, the settings checks (`settingsMergeMethods`, `settingsRequiredChecks` and `settingsReviewThreadResolution`). These take options and a layout as already validated and do not check them again; go through `runChecks` for validation. `parseWorkflow` and `loadWorkflows` expose the workflow model the workflow checks read. A check is a function that returns `Promise<readonly Violation[]>`, where a `Violation` is `{ code, message, file, location? }`.
|
|
297
|
+
Every check is also exported as a function taking `{ cwd, options }` (and `layout` for the import checks): `aggregateMappers`, `commandTypes`, `derivedTypes`, `importUphill`, `importRankSkip`, `importCrossSlice`, `importIsolatedGroups`, `importCycles`, `instructionSymlinks`, `singleStorybook`, `commitTypes`, `dockerfilePackageManager`, `migrationsDirectory`, `eslint`, the workflow checks (`workflowJobOrdering`, `workflowSkippableJobs`, `workflowRunnerResolution`, `workflowVersionSingleSource`, `workflowCredentials`, `workflowRepositoryDispatch`, `workflowUpdateBotCooldown`, `workflowMergeGroup` and `workflowActionPinning`) and, taking `{ cwd, options, github }`, the settings checks (`settingsMergeMethods`, `settingsRequiredChecks` and `settingsReviewThreadResolution`). These take options and a layout as already validated and do not check them again; go through `runChecks` for validation. `parseWorkflow` and `loadWorkflows` expose the workflow model the workflow checks read. A check is a function that returns `Promise<readonly Violation[]>`, where a `Violation` is `{ code, message, file, location? }`.
|
|
287
298
|
|
|
288
299
|
### Proving a handler map is exhaustive
|
|
289
300
|
|
package/dist/cli.js
CHANGED
|
@@ -5,7 +5,7 @@ import { parseArgs } from "node:util";
|
|
|
5
5
|
import { z } from "zod";
|
|
6
6
|
import { existsSync } from "node:fs";
|
|
7
7
|
import { escapePath, glob, isDynamicPattern } from "tinyglobby";
|
|
8
|
-
import { Node, Project } from "ts-morph";
|
|
8
|
+
import { Node, Project, SyntaxKind } from "ts-morph";
|
|
9
9
|
import { createJitiLoader, validateStandard } from "cosmiconfig-extends";
|
|
10
10
|
import { readFile, realpath } from "node:fs/promises";
|
|
11
11
|
import { execFile } from "node:child_process";
|
|
@@ -284,6 +284,32 @@ const aggregateMappers = async ({ cwd, options }) => {
|
|
|
284
284
|
return violations.sort((a, b) => a.file.localeCompare(b.file) || (a.location?.line ?? 0) - (b.location?.line ?? 0) || a.message.localeCompare(b.message));
|
|
285
285
|
};
|
|
286
286
|
//#endregion
|
|
287
|
+
//#region src/checks/type-references.ts
|
|
288
|
+
/**
|
|
289
|
+
* The identifier a reference ends in: `infer` in `z.infer`, the name itself when it is not qualified.
|
|
290
|
+
*/
|
|
291
|
+
function lastIdentifier(reference) {
|
|
292
|
+
if (Node.isQualifiedName(reference)) return reference.getRight();
|
|
293
|
+
if (Node.isPropertyAccessExpression(reference)) return reference.getNameNode();
|
|
294
|
+
return reference;
|
|
295
|
+
}
|
|
296
|
+
/**
|
|
297
|
+
* The declarations the identifier refers to, through any imports and re-exports.
|
|
298
|
+
*/
|
|
299
|
+
function declarationsOf(identifier) {
|
|
300
|
+
const symbol = identifier.getSymbol();
|
|
301
|
+
return (symbol?.getAliasedSymbol() ?? symbol)?.getDeclarations() ?? [];
|
|
302
|
+
}
|
|
303
|
+
/**
|
|
304
|
+
* The names a reference goes by: the last name as written, and the name it was declared with when that differs, as for `Infer` in `import type { infer as Infer } from 'zod'`, which is also `infer`. A library may itself re-export a generic under another name, so both count.
|
|
305
|
+
*/
|
|
306
|
+
function referenceNames(reference) {
|
|
307
|
+
const identifier = lastIdentifier(reference);
|
|
308
|
+
const symbol = identifier.getSymbol();
|
|
309
|
+
const declared = (symbol?.getAliasedSymbol() ?? symbol)?.getName();
|
|
310
|
+
return declared === void 0 ? [identifier.getText()] : [.../* @__PURE__ */ new Set([identifier.getText(), declared])];
|
|
311
|
+
}
|
|
312
|
+
//#endregion
|
|
287
313
|
//#region src/checks/command-types.ts
|
|
288
314
|
/**
|
|
289
315
|
* The last names of the generics that turn a schema into its type, used when the options name none: `z.infer`, `z.input`, `z.output`, `z.TypeOf` and Valibot's `InferInput` and `InferOutput`.
|
|
@@ -301,20 +327,10 @@ const DEFAULT_INFERENCES = [
|
|
|
301
327
|
*/
|
|
302
328
|
const STANDARD_SCHEMA_MEMBER = "~standard";
|
|
303
329
|
/**
|
|
304
|
-
* The names a type reference goes by: the last name as written, and the name it was declared with when that differs, as for `Infer` in `import type { infer as Infer } from 'zod'`, which is also `infer`. A library may itself re-export a generic under another name, so both count.
|
|
305
|
-
*/
|
|
306
|
-
function namesOf(typeName) {
|
|
307
|
-
const identifier = Node.isQualifiedName(typeName) ? typeName.getRight() : typeName;
|
|
308
|
-
const symbol = identifier.getSymbol();
|
|
309
|
-
const declared = (symbol?.getAliasedSymbol() ?? symbol)?.getName();
|
|
310
|
-
return declared === void 0 ? [identifier.getText()] : [.../* @__PURE__ */ new Set([identifier.getText(), declared])];
|
|
311
|
-
}
|
|
312
|
-
/**
|
|
313
330
|
* The local alias without type parameters that a type reference names, or `undefined` when it names anything else. Such an alias is a name for the type it is written as, so it is judged by that.
|
|
314
331
|
*/
|
|
315
332
|
function aliasNamed(typeName) {
|
|
316
|
-
const
|
|
317
|
-
const declaration = (symbol?.getAliasedSymbol() ?? symbol)?.getDeclarations().find(Node.isTypeAliasDeclaration);
|
|
333
|
+
const declaration = declarationsOf(lastIdentifier(typeName)).find(Node.isTypeAliasDeclaration);
|
|
318
334
|
return declaration?.getTypeParameters().length === 0 ? declaration : void 0;
|
|
319
335
|
}
|
|
320
336
|
/**
|
|
@@ -327,7 +343,7 @@ function problem$1(declaration, name, inferences, followed = /* @__PURE__ */ new
|
|
|
327
343
|
};
|
|
328
344
|
const written = declaration.getTypeNode();
|
|
329
345
|
if (Node.isTypeReference(written)) {
|
|
330
|
-
const inference =
|
|
346
|
+
const inference = referenceNames(written.getTypeName()).find((candidate) => inferences.includes(candidate));
|
|
331
347
|
if (inference !== void 0) {
|
|
332
348
|
const [argument] = written.getTypeArguments();
|
|
333
349
|
if (argument !== void 0 && Node.isTypeQuery(argument) && argument.getExprName().getType().getProperty(STANDARD_SCHEMA_MEMBER) !== void 0) return;
|
|
@@ -523,6 +539,120 @@ const commitTypes = async ({ cwd, options, configFiles }) => {
|
|
|
523
539
|
return violations;
|
|
524
540
|
};
|
|
525
541
|
//#endregion
|
|
542
|
+
//#region src/checks/derived-types.ts
|
|
543
|
+
/**
|
|
544
|
+
* The last names that count as inference when the options name none: those of `command-types`, Drizzle's `InferSelectModel` and `InferInsertModel` generics, and the `$inferSelect` and `$inferInsert` members of a Drizzle table, read as `typeof table.$inferSelect`.
|
|
545
|
+
*/
|
|
546
|
+
const DEFAULT_DERIVED_TYPE_INFERENCES = [
|
|
547
|
+
...DEFAULT_INFERENCES,
|
|
548
|
+
"InferSelectModel",
|
|
549
|
+
"InferInsertModel",
|
|
550
|
+
"$inferSelect",
|
|
551
|
+
"$inferInsert"
|
|
552
|
+
];
|
|
553
|
+
/**
|
|
554
|
+
* Whether the node names something declared in one of the schema files.
|
|
555
|
+
*/
|
|
556
|
+
function declaredInSchema(name, derivation) {
|
|
557
|
+
const identifier = Node.isIdentifier(name) ? name : Node.isQualifiedName(name) ? name.getRight() : void 0;
|
|
558
|
+
return identifier !== void 0 && declarationsOf(identifier).some((declaration) => derivation.schemaFiles.has(relativePosix(derivation.cwd, declaration.getSourceFile().getFilePath())));
|
|
559
|
+
}
|
|
560
|
+
/**
|
|
561
|
+
* The interface or type alias without type parameters a reference names, when it has not been followed yet. Such a declaration is a name for what it is written as, so it is judged by that.
|
|
562
|
+
*/
|
|
563
|
+
function followable(reference, derivation) {
|
|
564
|
+
const name = Node.isTypeReference(reference) ? reference.getTypeName() : reference.getExpression();
|
|
565
|
+
if (!Node.isIdentifier(name) && !Node.isQualifiedName(name) && !Node.isPropertyAccessExpression(name)) return;
|
|
566
|
+
const declaration = declarationsOf(lastIdentifier(name)).find((found) => Node.isTypeAliasDeclaration(found) || Node.isInterfaceDeclaration(found));
|
|
567
|
+
return declaration?.getTypeParameters().length === 0 && !derivation.followed.has(declaration) ? declaration : void 0;
|
|
568
|
+
}
|
|
569
|
+
function isNullish(node) {
|
|
570
|
+
return node.getKind() === SyntaxKind.UndefinedKeyword || Node.isLiteralTypeNode(node) && node.getLiteral().getKind() === SyntaxKind.NullKeyword;
|
|
571
|
+
}
|
|
572
|
+
/**
|
|
573
|
+
* Whether a reference is derived: an inference applied to `typeof schema`, an alias or interface that is itself derived, or another generic given a derived type, as `Partial<Order>` is.
|
|
574
|
+
*/
|
|
575
|
+
function derivedReference(reference, derivation) {
|
|
576
|
+
const name = Node.isTypeReference(reference) ? reference.getTypeName() : reference.getExpression();
|
|
577
|
+
if ((Node.isIdentifier(name) || Node.isQualifiedName(name) || Node.isPropertyAccessExpression(name)) && referenceNames(name).some((candidate) => derivation.inferences.has(candidate))) {
|
|
578
|
+
const [argument] = reference.getTypeArguments();
|
|
579
|
+
return argument !== void 0 && Node.isTypeQuery(argument) && declaredInSchema(argument.getExprName(), derivation);
|
|
580
|
+
}
|
|
581
|
+
const declaration = followable(reference, derivation);
|
|
582
|
+
if (declaration !== void 0) return derivedDeclaration(declaration, {
|
|
583
|
+
...derivation,
|
|
584
|
+
followed: /* @__PURE__ */ new Set([...derivation.followed, declaration])
|
|
585
|
+
});
|
|
586
|
+
return reference.getTypeArguments().some((argument) => derivedType(argument, derivation));
|
|
587
|
+
}
|
|
588
|
+
/**
|
|
589
|
+
* Whether a type, as written, is derived from a schema. A type literal is written by hand whatever its members are.
|
|
590
|
+
*/
|
|
591
|
+
function derivedType(node, derivation) {
|
|
592
|
+
if (Node.isTypeReference(node) || Node.isExpressionWithTypeArguments(node)) return derivedReference(node, derivation);
|
|
593
|
+
if (Node.isTypeQuery(node)) {
|
|
594
|
+
const name = node.getExprName();
|
|
595
|
+
return Node.isQualifiedName(name) && referenceNames(name).some((candidate) => derivation.inferences.has(candidate)) && declaredInSchema(name.getLeft(), derivation);
|
|
596
|
+
}
|
|
597
|
+
if (Node.isParenthesizedTypeNode(node) || Node.isTypeOperatorTypeNode(node)) return derivedType(node.getTypeNode(), derivation);
|
|
598
|
+
if (Node.isArrayTypeNode(node)) return derivedType(node.getElementTypeNode(), derivation);
|
|
599
|
+
if (Node.isIndexedAccessTypeNode(node)) return derivedType(node.getObjectTypeNode(), derivation);
|
|
600
|
+
if (Node.isIntersectionTypeNode(node)) return node.getTypeNodes().some((member) => derivedType(member, derivation));
|
|
601
|
+
if (Node.isUnionTypeNode(node)) {
|
|
602
|
+
const members = node.getTypeNodes().filter((member) => !isNullish(member));
|
|
603
|
+
return members.length > 0 && members.every((member) => derivedType(member, derivation));
|
|
604
|
+
}
|
|
605
|
+
return false;
|
|
606
|
+
}
|
|
607
|
+
/**
|
|
608
|
+
* Whether an interface or type alias is derived: an alias by the type it is written as, an interface by whether it extends a derived type.
|
|
609
|
+
*/
|
|
610
|
+
function derivedDeclaration(declaration, derivation) {
|
|
611
|
+
if (Node.isInterfaceDeclaration(declaration)) return declaration.getExtends().some((heritage) => derivedReference(heritage, derivation));
|
|
612
|
+
return derivedType(declaration.getTypeNodeOrThrow(), derivation);
|
|
613
|
+
}
|
|
614
|
+
async function pairViolations(cwd, options, pair, index) {
|
|
615
|
+
const { project, files } = await createProject(cwd, options.tsConfig, pair.types, {
|
|
616
|
+
check: "derived-types",
|
|
617
|
+
files: `pairs.${String(index)}.types`
|
|
618
|
+
});
|
|
619
|
+
const schemaFiles = await findFiles(cwd, pair.schemas);
|
|
620
|
+
if (schemaFiles.length === 0) throw new ConformanceError(`checks.derived-types.pairs.${String(index)}.schemas: no file matches`);
|
|
621
|
+
const excluded = new Set(options.exclude);
|
|
622
|
+
const derivation = {
|
|
623
|
+
inferences: new Set(options.inferences ?? DEFAULT_DERIVED_TYPE_INFERENCES),
|
|
624
|
+
schemaFiles: new Set(schemaFiles),
|
|
625
|
+
cwd,
|
|
626
|
+
followed: /* @__PURE__ */ new Set()
|
|
627
|
+
};
|
|
628
|
+
const seen = /* @__PURE__ */ new Set();
|
|
629
|
+
const violations = [];
|
|
630
|
+
for (const file of files) for (const [name, declaration] of exportedTypes(project.getSourceFileOrThrow(resolve(cwd, file)))) {
|
|
631
|
+
if (excluded.has(name) || seen.has(declaration)) continue;
|
|
632
|
+
seen.add(declaration);
|
|
633
|
+
if (!derivedDeclaration(declaration, {
|
|
634
|
+
...derivation,
|
|
635
|
+
followed: /* @__PURE__ */ new Set([declaration])
|
|
636
|
+
})) violations.push({
|
|
637
|
+
code: "derived-types/not-derived",
|
|
638
|
+
message: `${name} is not derived from a schema in ${pair.schemas.join(", ")}: nothing in its definition applies an inference generic to one`,
|
|
639
|
+
file: relativePosix(cwd, declaration.getSourceFile().getFilePath()),
|
|
640
|
+
location: declarationLocation(declaration)
|
|
641
|
+
});
|
|
642
|
+
}
|
|
643
|
+
return violations;
|
|
644
|
+
}
|
|
645
|
+
/**
|
|
646
|
+
* For each pair, every exported interface and type alias of its type files is derived from a schema declared in its schema files: somewhere in its definition an inference (a generic in `inferences`, such as `z.infer` or `InferSelectModel`) is applied to `typeof schema`, or an inference member is read from it (`typeof table.$inferSelect`).
|
|
647
|
+
*
|
|
648
|
+
* The definition is followed through aliases and interfaces without type parameters, in any file, and through the arguments of other generics, intersections, unions whose members are all derived (`null` and `undefined` aside), arrays, type operators and indexed access. A type literal, and a schema passed to a generic that is not an inference, are written by hand.
|
|
649
|
+
*/
|
|
650
|
+
const derivedTypes = async ({ cwd, options }) => {
|
|
651
|
+
const violations = [];
|
|
652
|
+
for (const [index, pair] of options.pairs.entries()) violations.push(...await pairViolations(cwd, options, pair, index));
|
|
653
|
+
return violations.sort((a, b) => a.file.localeCompare(b.file) || (a.location?.line ?? 0) - (b.location?.line ?? 0) || a.message.localeCompare(b.message));
|
|
654
|
+
};
|
|
655
|
+
//#endregion
|
|
526
656
|
//#region src/checks/dockerfile-package-manager.ts
|
|
527
657
|
const DEFAULT_DOCKERFILES = [
|
|
528
658
|
"**/Dockerfile",
|
|
@@ -3034,6 +3164,12 @@ const registry = {
|
|
|
3034
3164
|
select: (checks) => enabledOptions(checks["command-types"]),
|
|
3035
3165
|
check: commandTypes
|
|
3036
3166
|
}),
|
|
3167
|
+
"derived-types": plain({
|
|
3168
|
+
name: "derived-types",
|
|
3169
|
+
description: "Every exported type in a file paired with schema files is derived from one of those schemas, not hand-written",
|
|
3170
|
+
select: (checks) => enabledOptions(checks["derived-types"]),
|
|
3171
|
+
check: derivedTypes
|
|
3172
|
+
}),
|
|
3037
3173
|
"import-uphill": importCheck({
|
|
3038
3174
|
name: "import-uphill",
|
|
3039
3175
|
description: "No package imports a package of a higher rank",
|
|
@@ -3239,6 +3375,15 @@ const checks = z.strictObject({
|
|
|
3239
3375
|
inferences: z.exactOptional(paths),
|
|
3240
3376
|
tsConfig: z.exactOptional(path)
|
|
3241
3377
|
}))),
|
|
3378
|
+
"derived-types": z.exactOptional(enabled(z.strictObject({
|
|
3379
|
+
pairs: z.array(z.strictObject({
|
|
3380
|
+
schemas: paths,
|
|
3381
|
+
types: paths
|
|
3382
|
+
})).min(1),
|
|
3383
|
+
exclude: z.exactOptional(names),
|
|
3384
|
+
inferences: z.exactOptional(paths),
|
|
3385
|
+
tsConfig: z.exactOptional(path)
|
|
3386
|
+
}))),
|
|
3242
3387
|
"import-uphill": z.exactOptional(enabled(importGraph)),
|
|
3243
3388
|
"import-rank-skip": z.exactOptional(enabled(importGraph)),
|
|
3244
3389
|
"import-cross-slice": z.exactOptional(enabled(importGraph)),
|
package/dist/index.cjs
CHANGED
|
@@ -213,6 +213,32 @@ const aggregateMappers = async ({ cwd, options }) => {
|
|
|
213
213
|
return violations.sort((a, b) => a.file.localeCompare(b.file) || (a.location?.line ?? 0) - (b.location?.line ?? 0) || a.message.localeCompare(b.message));
|
|
214
214
|
};
|
|
215
215
|
//#endregion
|
|
216
|
+
//#region src/checks/type-references.ts
|
|
217
|
+
/**
|
|
218
|
+
* The identifier a reference ends in: `infer` in `z.infer`, the name itself when it is not qualified.
|
|
219
|
+
*/
|
|
220
|
+
function lastIdentifier(reference) {
|
|
221
|
+
if (ts_morph.Node.isQualifiedName(reference)) return reference.getRight();
|
|
222
|
+
if (ts_morph.Node.isPropertyAccessExpression(reference)) return reference.getNameNode();
|
|
223
|
+
return reference;
|
|
224
|
+
}
|
|
225
|
+
/**
|
|
226
|
+
* The declarations the identifier refers to, through any imports and re-exports.
|
|
227
|
+
*/
|
|
228
|
+
function declarationsOf(identifier) {
|
|
229
|
+
const symbol = identifier.getSymbol();
|
|
230
|
+
return (symbol?.getAliasedSymbol() ?? symbol)?.getDeclarations() ?? [];
|
|
231
|
+
}
|
|
232
|
+
/**
|
|
233
|
+
* The names a reference goes by: the last name as written, and the name it was declared with when that differs, as for `Infer` in `import type { infer as Infer } from 'zod'`, which is also `infer`. A library may itself re-export a generic under another name, so both count.
|
|
234
|
+
*/
|
|
235
|
+
function referenceNames(reference) {
|
|
236
|
+
const identifier = lastIdentifier(reference);
|
|
237
|
+
const symbol = identifier.getSymbol();
|
|
238
|
+
const declared = (symbol?.getAliasedSymbol() ?? symbol)?.getName();
|
|
239
|
+
return declared === void 0 ? [identifier.getText()] : [.../* @__PURE__ */ new Set([identifier.getText(), declared])];
|
|
240
|
+
}
|
|
241
|
+
//#endregion
|
|
216
242
|
//#region src/checks/command-types.ts
|
|
217
243
|
/**
|
|
218
244
|
* The last names of the generics that turn a schema into its type, used when the options name none: `z.infer`, `z.input`, `z.output`, `z.TypeOf` and Valibot's `InferInput` and `InferOutput`.
|
|
@@ -230,20 +256,10 @@ const DEFAULT_INFERENCES = [
|
|
|
230
256
|
*/
|
|
231
257
|
const STANDARD_SCHEMA_MEMBER = "~standard";
|
|
232
258
|
/**
|
|
233
|
-
* The names a type reference goes by: the last name as written, and the name it was declared with when that differs, as for `Infer` in `import type { infer as Infer } from 'zod'`, which is also `infer`. A library may itself re-export a generic under another name, so both count.
|
|
234
|
-
*/
|
|
235
|
-
function namesOf(typeName) {
|
|
236
|
-
const identifier = ts_morph.Node.isQualifiedName(typeName) ? typeName.getRight() : typeName;
|
|
237
|
-
const symbol = identifier.getSymbol();
|
|
238
|
-
const declared = (symbol?.getAliasedSymbol() ?? symbol)?.getName();
|
|
239
|
-
return declared === void 0 ? [identifier.getText()] : [.../* @__PURE__ */ new Set([identifier.getText(), declared])];
|
|
240
|
-
}
|
|
241
|
-
/**
|
|
242
259
|
* The local alias without type parameters that a type reference names, or `undefined` when it names anything else. Such an alias is a name for the type it is written as, so it is judged by that.
|
|
243
260
|
*/
|
|
244
261
|
function aliasNamed(typeName) {
|
|
245
|
-
const
|
|
246
|
-
const declaration = (symbol?.getAliasedSymbol() ?? symbol)?.getDeclarations().find(ts_morph.Node.isTypeAliasDeclaration);
|
|
262
|
+
const declaration = declarationsOf(lastIdentifier(typeName)).find(ts_morph.Node.isTypeAliasDeclaration);
|
|
247
263
|
return declaration?.getTypeParameters().length === 0 ? declaration : void 0;
|
|
248
264
|
}
|
|
249
265
|
/**
|
|
@@ -256,7 +272,7 @@ function problem$1(declaration, name, inferences, followed = /* @__PURE__ */ new
|
|
|
256
272
|
};
|
|
257
273
|
const written = declaration.getTypeNode();
|
|
258
274
|
if (ts_morph.Node.isTypeReference(written)) {
|
|
259
|
-
const inference =
|
|
275
|
+
const inference = referenceNames(written.getTypeName()).find((candidate) => inferences.includes(candidate));
|
|
260
276
|
if (inference !== void 0) {
|
|
261
277
|
const [argument] = written.getTypeArguments();
|
|
262
278
|
if (argument !== void 0 && ts_morph.Node.isTypeQuery(argument) && argument.getExprName().getType().getProperty(STANDARD_SCHEMA_MEMBER) !== void 0) return;
|
|
@@ -452,6 +468,120 @@ const commitTypes = async ({ cwd, options, configFiles }) => {
|
|
|
452
468
|
return violations;
|
|
453
469
|
};
|
|
454
470
|
//#endregion
|
|
471
|
+
//#region src/checks/derived-types.ts
|
|
472
|
+
/**
|
|
473
|
+
* The last names that count as inference when the options name none: those of `command-types`, Drizzle's `InferSelectModel` and `InferInsertModel` generics, and the `$inferSelect` and `$inferInsert` members of a Drizzle table, read as `typeof table.$inferSelect`.
|
|
474
|
+
*/
|
|
475
|
+
const DEFAULT_DERIVED_TYPE_INFERENCES = [
|
|
476
|
+
...DEFAULT_INFERENCES,
|
|
477
|
+
"InferSelectModel",
|
|
478
|
+
"InferInsertModel",
|
|
479
|
+
"$inferSelect",
|
|
480
|
+
"$inferInsert"
|
|
481
|
+
];
|
|
482
|
+
/**
|
|
483
|
+
* Whether the node names something declared in one of the schema files.
|
|
484
|
+
*/
|
|
485
|
+
function declaredInSchema(name, derivation) {
|
|
486
|
+
const identifier = ts_morph.Node.isIdentifier(name) ? name : ts_morph.Node.isQualifiedName(name) ? name.getRight() : void 0;
|
|
487
|
+
return identifier !== void 0 && declarationsOf(identifier).some((declaration) => derivation.schemaFiles.has(relativePosix(derivation.cwd, declaration.getSourceFile().getFilePath())));
|
|
488
|
+
}
|
|
489
|
+
/**
|
|
490
|
+
* The interface or type alias without type parameters a reference names, when it has not been followed yet. Such a declaration is a name for what it is written as, so it is judged by that.
|
|
491
|
+
*/
|
|
492
|
+
function followable(reference, derivation) {
|
|
493
|
+
const name = ts_morph.Node.isTypeReference(reference) ? reference.getTypeName() : reference.getExpression();
|
|
494
|
+
if (!ts_morph.Node.isIdentifier(name) && !ts_morph.Node.isQualifiedName(name) && !ts_morph.Node.isPropertyAccessExpression(name)) return;
|
|
495
|
+
const declaration = declarationsOf(lastIdentifier(name)).find((found) => ts_morph.Node.isTypeAliasDeclaration(found) || ts_morph.Node.isInterfaceDeclaration(found));
|
|
496
|
+
return declaration?.getTypeParameters().length === 0 && !derivation.followed.has(declaration) ? declaration : void 0;
|
|
497
|
+
}
|
|
498
|
+
function isNullish(node) {
|
|
499
|
+
return node.getKind() === ts_morph.SyntaxKind.UndefinedKeyword || ts_morph.Node.isLiteralTypeNode(node) && node.getLiteral().getKind() === ts_morph.SyntaxKind.NullKeyword;
|
|
500
|
+
}
|
|
501
|
+
/**
|
|
502
|
+
* Whether a reference is derived: an inference applied to `typeof schema`, an alias or interface that is itself derived, or another generic given a derived type, as `Partial<Order>` is.
|
|
503
|
+
*/
|
|
504
|
+
function derivedReference(reference, derivation) {
|
|
505
|
+
const name = ts_morph.Node.isTypeReference(reference) ? reference.getTypeName() : reference.getExpression();
|
|
506
|
+
if ((ts_morph.Node.isIdentifier(name) || ts_morph.Node.isQualifiedName(name) || ts_morph.Node.isPropertyAccessExpression(name)) && referenceNames(name).some((candidate) => derivation.inferences.has(candidate))) {
|
|
507
|
+
const [argument] = reference.getTypeArguments();
|
|
508
|
+
return argument !== void 0 && ts_morph.Node.isTypeQuery(argument) && declaredInSchema(argument.getExprName(), derivation);
|
|
509
|
+
}
|
|
510
|
+
const declaration = followable(reference, derivation);
|
|
511
|
+
if (declaration !== void 0) return derivedDeclaration(declaration, {
|
|
512
|
+
...derivation,
|
|
513
|
+
followed: /* @__PURE__ */ new Set([...derivation.followed, declaration])
|
|
514
|
+
});
|
|
515
|
+
return reference.getTypeArguments().some((argument) => derivedType(argument, derivation));
|
|
516
|
+
}
|
|
517
|
+
/**
|
|
518
|
+
* Whether a type, as written, is derived from a schema. A type literal is written by hand whatever its members are.
|
|
519
|
+
*/
|
|
520
|
+
function derivedType(node, derivation) {
|
|
521
|
+
if (ts_morph.Node.isTypeReference(node) || ts_morph.Node.isExpressionWithTypeArguments(node)) return derivedReference(node, derivation);
|
|
522
|
+
if (ts_morph.Node.isTypeQuery(node)) {
|
|
523
|
+
const name = node.getExprName();
|
|
524
|
+
return ts_morph.Node.isQualifiedName(name) && referenceNames(name).some((candidate) => derivation.inferences.has(candidate)) && declaredInSchema(name.getLeft(), derivation);
|
|
525
|
+
}
|
|
526
|
+
if (ts_morph.Node.isParenthesizedTypeNode(node) || ts_morph.Node.isTypeOperatorTypeNode(node)) return derivedType(node.getTypeNode(), derivation);
|
|
527
|
+
if (ts_morph.Node.isArrayTypeNode(node)) return derivedType(node.getElementTypeNode(), derivation);
|
|
528
|
+
if (ts_morph.Node.isIndexedAccessTypeNode(node)) return derivedType(node.getObjectTypeNode(), derivation);
|
|
529
|
+
if (ts_morph.Node.isIntersectionTypeNode(node)) return node.getTypeNodes().some((member) => derivedType(member, derivation));
|
|
530
|
+
if (ts_morph.Node.isUnionTypeNode(node)) {
|
|
531
|
+
const members = node.getTypeNodes().filter((member) => !isNullish(member));
|
|
532
|
+
return members.length > 0 && members.every((member) => derivedType(member, derivation));
|
|
533
|
+
}
|
|
534
|
+
return false;
|
|
535
|
+
}
|
|
536
|
+
/**
|
|
537
|
+
* Whether an interface or type alias is derived: an alias by the type it is written as, an interface by whether it extends a derived type.
|
|
538
|
+
*/
|
|
539
|
+
function derivedDeclaration(declaration, derivation) {
|
|
540
|
+
if (ts_morph.Node.isInterfaceDeclaration(declaration)) return declaration.getExtends().some((heritage) => derivedReference(heritage, derivation));
|
|
541
|
+
return derivedType(declaration.getTypeNodeOrThrow(), derivation);
|
|
542
|
+
}
|
|
543
|
+
async function pairViolations(cwd, options, pair, index) {
|
|
544
|
+
const { project, files } = await createProject(cwd, options.tsConfig, pair.types, {
|
|
545
|
+
check: "derived-types",
|
|
546
|
+
files: `pairs.${String(index)}.types`
|
|
547
|
+
});
|
|
548
|
+
const schemaFiles = await findFiles(cwd, pair.schemas);
|
|
549
|
+
if (schemaFiles.length === 0) throw new ConformanceError(`checks.derived-types.pairs.${String(index)}.schemas: no file matches`);
|
|
550
|
+
const excluded = new Set(options.exclude);
|
|
551
|
+
const derivation = {
|
|
552
|
+
inferences: new Set(options.inferences ?? DEFAULT_DERIVED_TYPE_INFERENCES),
|
|
553
|
+
schemaFiles: new Set(schemaFiles),
|
|
554
|
+
cwd,
|
|
555
|
+
followed: /* @__PURE__ */ new Set()
|
|
556
|
+
};
|
|
557
|
+
const seen = /* @__PURE__ */ new Set();
|
|
558
|
+
const violations = [];
|
|
559
|
+
for (const file of files) for (const [name, declaration] of exportedTypes(project.getSourceFileOrThrow((0, node_path.resolve)(cwd, file)))) {
|
|
560
|
+
if (excluded.has(name) || seen.has(declaration)) continue;
|
|
561
|
+
seen.add(declaration);
|
|
562
|
+
if (!derivedDeclaration(declaration, {
|
|
563
|
+
...derivation,
|
|
564
|
+
followed: /* @__PURE__ */ new Set([declaration])
|
|
565
|
+
})) violations.push({
|
|
566
|
+
code: "derived-types/not-derived",
|
|
567
|
+
message: `${name} is not derived from a schema in ${pair.schemas.join(", ")}: nothing in its definition applies an inference generic to one`,
|
|
568
|
+
file: relativePosix(cwd, declaration.getSourceFile().getFilePath()),
|
|
569
|
+
location: declarationLocation(declaration)
|
|
570
|
+
});
|
|
571
|
+
}
|
|
572
|
+
return violations;
|
|
573
|
+
}
|
|
574
|
+
/**
|
|
575
|
+
* For each pair, every exported interface and type alias of its type files is derived from a schema declared in its schema files: somewhere in its definition an inference (a generic in `inferences`, such as `z.infer` or `InferSelectModel`) is applied to `typeof schema`, or an inference member is read from it (`typeof table.$inferSelect`).
|
|
576
|
+
*
|
|
577
|
+
* The definition is followed through aliases and interfaces without type parameters, in any file, and through the arguments of other generics, intersections, unions whose members are all derived (`null` and `undefined` aside), arrays, type operators and indexed access. A type literal, and a schema passed to a generic that is not an inference, are written by hand.
|
|
578
|
+
*/
|
|
579
|
+
const derivedTypes = async ({ cwd, options }) => {
|
|
580
|
+
const violations = [];
|
|
581
|
+
for (const [index, pair] of options.pairs.entries()) violations.push(...await pairViolations(cwd, options, pair, index));
|
|
582
|
+
return violations.sort((a, b) => a.file.localeCompare(b.file) || (a.location?.line ?? 0) - (b.location?.line ?? 0) || a.message.localeCompare(b.message));
|
|
583
|
+
};
|
|
584
|
+
//#endregion
|
|
455
585
|
//#region src/checks/dockerfile-package-manager.ts
|
|
456
586
|
const DEFAULT_DOCKERFILES = [
|
|
457
587
|
"**/Dockerfile",
|
|
@@ -2979,6 +3109,15 @@ const checks = zod.z.strictObject({
|
|
|
2979
3109
|
inferences: zod.z.exactOptional(paths),
|
|
2980
3110
|
tsConfig: zod.z.exactOptional(path)
|
|
2981
3111
|
}))),
|
|
3112
|
+
"derived-types": zod.z.exactOptional(enabled(zod.z.strictObject({
|
|
3113
|
+
pairs: zod.z.array(zod.z.strictObject({
|
|
3114
|
+
schemas: paths,
|
|
3115
|
+
types: paths
|
|
3116
|
+
})).min(1),
|
|
3117
|
+
exclude: zod.z.exactOptional(names),
|
|
3118
|
+
inferences: zod.z.exactOptional(paths),
|
|
3119
|
+
tsConfig: zod.z.exactOptional(path)
|
|
3120
|
+
}))),
|
|
2982
3121
|
"import-uphill": zod.z.exactOptional(enabled(importGraph)),
|
|
2983
3122
|
"import-rank-skip": zod.z.exactOptional(enabled(importGraph)),
|
|
2984
3123
|
"import-cross-slice": zod.z.exactOptional(enabled(importGraph)),
|
|
@@ -3226,6 +3365,12 @@ const registry = {
|
|
|
3226
3365
|
select: (checks) => enabledOptions(checks["command-types"]),
|
|
3227
3366
|
check: commandTypes
|
|
3228
3367
|
}),
|
|
3368
|
+
"derived-types": plain({
|
|
3369
|
+
name: "derived-types",
|
|
3370
|
+
description: "Every exported type in a file paired with schema files is derived from one of those schemas, not hand-written",
|
|
3371
|
+
select: (checks) => enabledOptions(checks["derived-types"]),
|
|
3372
|
+
check: derivedTypes
|
|
3373
|
+
}),
|
|
3229
3374
|
"import-uphill": importCheck({
|
|
3230
3375
|
name: "import-uphill",
|
|
3231
3376
|
description: "No package imports a package of a higher rank",
|
|
@@ -3574,6 +3719,7 @@ function proveExhaustive(input) {
|
|
|
3574
3719
|
}
|
|
3575
3720
|
//#endregion
|
|
3576
3721
|
exports.ConformanceError = ConformanceError;
|
|
3722
|
+
exports.DEFAULT_DERIVED_TYPE_INFERENCES = DEFAULT_DERIVED_TYPE_INFERENCES;
|
|
3577
3723
|
exports.DEFAULT_INFERENCES = DEFAULT_INFERENCES;
|
|
3578
3724
|
exports.DEFAULT_WORKFLOWS = DEFAULT_WORKFLOWS;
|
|
3579
3725
|
exports.EXIT_CODES = EXIT_CODES;
|
|
@@ -3591,6 +3737,7 @@ exports.createGitHubClient = createGitHubClient;
|
|
|
3591
3737
|
exports.crossSliceRules = crossSliceRules;
|
|
3592
3738
|
exports.cycleRule = cycleRule;
|
|
3593
3739
|
exports.deployTools = deployTools;
|
|
3740
|
+
exports.derivedTypes = derivedTypes;
|
|
3594
3741
|
exports.dockerfilePackageManager = dockerfilePackageManager;
|
|
3595
3742
|
exports.effectiveEnv = effectiveEnv;
|
|
3596
3743
|
exports.effectivePermissions = effectivePermissions;
|
package/dist/index.d.cts
CHANGED
|
@@ -397,6 +397,40 @@ interface CommandTypesOptions {
|
|
|
397
397
|
*/
|
|
398
398
|
readonly tsConfig?: string;
|
|
399
399
|
}
|
|
400
|
+
/**
|
|
401
|
+
* A set of schema files and the files whose exported types must be derived from those schemas.
|
|
402
|
+
*/
|
|
403
|
+
interface DerivedTypesPair {
|
|
404
|
+
/**
|
|
405
|
+
* Globs of the files that declare the schemas, relative to the directory the checks run in.
|
|
406
|
+
*/
|
|
407
|
+
readonly schemas: readonly string[];
|
|
408
|
+
/**
|
|
409
|
+
* Globs of the files whose exported interfaces and type aliases must be derived from a schema declared in `schemas`.
|
|
410
|
+
*/
|
|
411
|
+
readonly types: readonly string[];
|
|
412
|
+
}
|
|
413
|
+
/**
|
|
414
|
+
* Options of the `derived-types` check: every type a file exports is derived from a schema of its pair.
|
|
415
|
+
*/
|
|
416
|
+
interface DerivedTypesOptions {
|
|
417
|
+
/**
|
|
418
|
+
* The schema files and the type files that must derive from them. Each type file is judged against the schemas of every pair that lists it.
|
|
419
|
+
*/
|
|
420
|
+
readonly pairs: readonly DerivedTypesPair[];
|
|
421
|
+
/**
|
|
422
|
+
* Names of exported types that are deliberately written by hand.
|
|
423
|
+
*/
|
|
424
|
+
readonly exclude?: readonly string[];
|
|
425
|
+
/**
|
|
426
|
+
* The last names of the generics that turn a schema into its type, and of the members that hold it, such as `infer` in `z.infer<typeof schema>` and `$inferSelect` in `typeof table.$inferSelect`. `infer`, `input`, `output`, `TypeOf`, `InferInput`, `InferOutput`, `InferSelectModel`, `InferInsertModel`, `$inferSelect` and `$inferInsert` when omitted.
|
|
427
|
+
*/
|
|
428
|
+
readonly inferences?: readonly string[];
|
|
429
|
+
/**
|
|
430
|
+
* The tsconfig that supplies compiler options and module resolution; `tsconfig.json` when omitted.
|
|
431
|
+
*/
|
|
432
|
+
readonly tsConfig?: string;
|
|
433
|
+
}
|
|
400
434
|
/**
|
|
401
435
|
* Options shared by the checks that cruise the workspace's import graph.
|
|
402
436
|
*/
|
|
@@ -704,6 +738,7 @@ type SettingsReviewThreadResolutionOptions = SettingsOptions;
|
|
|
704
738
|
interface CheckOptionsByName {
|
|
705
739
|
readonly "aggregate-mappers": AggregateMappersOptions;
|
|
706
740
|
readonly "command-types": CommandTypesOptions;
|
|
741
|
+
readonly "derived-types": DerivedTypesOptions;
|
|
707
742
|
readonly "import-uphill": ImportGraphOptions;
|
|
708
743
|
readonly "import-rank-skip": ImportGraphOptions;
|
|
709
744
|
readonly "import-cross-slice": ImportGraphOptions;
|
|
@@ -765,6 +800,18 @@ export declare const PRESET_COMMIT_TYPES: readonly string[];
|
|
|
765
800
|
*/
|
|
766
801
|
export declare const commitTypes: CheckFunction<CommitTypesOptions>;
|
|
767
802
|
//#endregion
|
|
803
|
+
//#region src/checks/derived-types.d.ts
|
|
804
|
+
/**
|
|
805
|
+
* The last names that count as inference when the options name none: those of `command-types`, Drizzle's `InferSelectModel` and `InferInsertModel` generics, and the `$inferSelect` and `$inferInsert` members of a Drizzle table, read as `typeof table.$inferSelect`.
|
|
806
|
+
*/
|
|
807
|
+
export declare const DEFAULT_DERIVED_TYPE_INFERENCES: readonly string[];
|
|
808
|
+
/**
|
|
809
|
+
* For each pair, every exported interface and type alias of its type files is derived from a schema declared in its schema files: somewhere in its definition an inference (a generic in `inferences`, such as `z.infer` or `InferSelectModel`) is applied to `typeof schema`, or an inference member is read from it (`typeof table.$inferSelect`).
|
|
810
|
+
*
|
|
811
|
+
* The definition is followed through aliases and interfaces without type parameters, in any file, and through the arguments of other generics, intersections, unions whose members are all derived (`null` and `undefined` aside), arrays, type operators and indexed access. A type literal, and a schema passed to a generic that is not an inference, are written by hand.
|
|
812
|
+
*/
|
|
813
|
+
export declare const derivedTypes: CheckFunction<DerivedTypesOptions>;
|
|
814
|
+
//#endregion
|
|
768
815
|
//#region src/checks/dockerfile-package-manager.d.ts
|
|
769
816
|
/**
|
|
770
817
|
* Every pin of the package manager that `packageManager` names, in each Dockerfile, is the version `packageManager` names, with any `+sha` integrity suffix ignored on both sides. Pins of other package managers are not the workspace's package manager and are left alone.
|
|
@@ -1275,4 +1322,4 @@ export declare function proveExhaustive(input: {
|
|
|
1275
1322
|
readonly compilerOptions?: CompilerOptions;
|
|
1276
1323
|
}): ExhaustivenessProof;
|
|
1277
1324
|
//#endregion
|
|
1278
|
-
export type { AggregateMappersOptions, BranchRules, CaseRemoval, CheckContext, CheckFunction, CheckName, CheckOptionsByName, CheckResult, ChecksConfig, CommandTypesOptions, CommitTypesOptions, ConformanceConfig, DeployDirectory, DeploySelector, DeployToolAdapter, DeployToolName, DiscoveredPackage, DockerfilePackageManagerOptions, EslintOptions, EslintRequiredSeverity, EslintSample, ExhaustivenessProof, GeneratorAdapter, GeneratorName, GitHubCheckContext, GitHubCheckFunction, GitHubClient, GitHubClientOptions, ImportGraphOptions, InstructionSymlinksOptions, Job, LayoutCheckContext, LayoutCheckFunction, MigrationsDirectoryOptions, PermissionAccess, Permissions, PinningLevel, RegisteredCheck, RepositorySettings, RepositorySlug, RunChecksOptions, RunResult, RunsOn, SettingsMergeMethodsOptions, SettingsOptions, SettingsRequiredChecksOptions, SettingsReviewThreadResolutionOptions, SingleStorybookOptions, SourceLocation, Step, TypeErrorReport, UsesReference, Violation, Workflow, WorkflowActionPinningOptions, WorkflowCredentialsOptions, WorkflowJobOrderingOptions, WorkflowMergeGroupOptions, WorkflowRepositoryDispatchOptions, WorkflowRunnerResolutionOptions, WorkflowSelection, WorkflowSkippableJobsOptions, WorkflowUpdateBotCooldownOptions, WorkflowVersionSingleSourceOptions, WorkspacePackage };
|
|
1325
|
+
export type { AggregateMappersOptions, BranchRules, CaseRemoval, CheckContext, CheckFunction, CheckName, CheckOptionsByName, CheckResult, ChecksConfig, CommandTypesOptions, CommitTypesOptions, ConformanceConfig, DeployDirectory, DeploySelector, DeployToolAdapter, DeployToolName, DerivedTypesOptions, DerivedTypesPair, DiscoveredPackage, DockerfilePackageManagerOptions, EslintOptions, EslintRequiredSeverity, EslintSample, ExhaustivenessProof, GeneratorAdapter, GeneratorName, GitHubCheckContext, GitHubCheckFunction, GitHubClient, GitHubClientOptions, ImportGraphOptions, InstructionSymlinksOptions, Job, LayoutCheckContext, LayoutCheckFunction, MigrationsDirectoryOptions, PermissionAccess, Permissions, PinningLevel, RegisteredCheck, RepositorySettings, RepositorySlug, RunChecksOptions, RunResult, RunsOn, SettingsMergeMethodsOptions, SettingsOptions, SettingsRequiredChecksOptions, SettingsReviewThreadResolutionOptions, SingleStorybookOptions, SourceLocation, Step, TypeErrorReport, UsesReference, Violation, Workflow, WorkflowActionPinningOptions, WorkflowCredentialsOptions, WorkflowJobOrderingOptions, WorkflowMergeGroupOptions, WorkflowRepositoryDispatchOptions, WorkflowRunnerResolutionOptions, WorkflowSelection, WorkflowSkippableJobsOptions, WorkflowUpdateBotCooldownOptions, WorkflowVersionSingleSourceOptions, WorkspacePackage };
|
package/dist/index.d.ts
CHANGED
|
@@ -397,6 +397,40 @@ interface CommandTypesOptions {
|
|
|
397
397
|
*/
|
|
398
398
|
readonly tsConfig?: string;
|
|
399
399
|
}
|
|
400
|
+
/**
|
|
401
|
+
* A set of schema files and the files whose exported types must be derived from those schemas.
|
|
402
|
+
*/
|
|
403
|
+
interface DerivedTypesPair {
|
|
404
|
+
/**
|
|
405
|
+
* Globs of the files that declare the schemas, relative to the directory the checks run in.
|
|
406
|
+
*/
|
|
407
|
+
readonly schemas: readonly string[];
|
|
408
|
+
/**
|
|
409
|
+
* Globs of the files whose exported interfaces and type aliases must be derived from a schema declared in `schemas`.
|
|
410
|
+
*/
|
|
411
|
+
readonly types: readonly string[];
|
|
412
|
+
}
|
|
413
|
+
/**
|
|
414
|
+
* Options of the `derived-types` check: every type a file exports is derived from a schema of its pair.
|
|
415
|
+
*/
|
|
416
|
+
interface DerivedTypesOptions {
|
|
417
|
+
/**
|
|
418
|
+
* The schema files and the type files that must derive from them. Each type file is judged against the schemas of every pair that lists it.
|
|
419
|
+
*/
|
|
420
|
+
readonly pairs: readonly DerivedTypesPair[];
|
|
421
|
+
/**
|
|
422
|
+
* Names of exported types that are deliberately written by hand.
|
|
423
|
+
*/
|
|
424
|
+
readonly exclude?: readonly string[];
|
|
425
|
+
/**
|
|
426
|
+
* The last names of the generics that turn a schema into its type, and of the members that hold it, such as `infer` in `z.infer<typeof schema>` and `$inferSelect` in `typeof table.$inferSelect`. `infer`, `input`, `output`, `TypeOf`, `InferInput`, `InferOutput`, `InferSelectModel`, `InferInsertModel`, `$inferSelect` and `$inferInsert` when omitted.
|
|
427
|
+
*/
|
|
428
|
+
readonly inferences?: readonly string[];
|
|
429
|
+
/**
|
|
430
|
+
* The tsconfig that supplies compiler options and module resolution; `tsconfig.json` when omitted.
|
|
431
|
+
*/
|
|
432
|
+
readonly tsConfig?: string;
|
|
433
|
+
}
|
|
400
434
|
/**
|
|
401
435
|
* Options shared by the checks that cruise the workspace's import graph.
|
|
402
436
|
*/
|
|
@@ -704,6 +738,7 @@ type SettingsReviewThreadResolutionOptions = SettingsOptions;
|
|
|
704
738
|
interface CheckOptionsByName {
|
|
705
739
|
readonly "aggregate-mappers": AggregateMappersOptions;
|
|
706
740
|
readonly "command-types": CommandTypesOptions;
|
|
741
|
+
readonly "derived-types": DerivedTypesOptions;
|
|
707
742
|
readonly "import-uphill": ImportGraphOptions;
|
|
708
743
|
readonly "import-rank-skip": ImportGraphOptions;
|
|
709
744
|
readonly "import-cross-slice": ImportGraphOptions;
|
|
@@ -765,6 +800,18 @@ export declare const PRESET_COMMIT_TYPES: readonly string[];
|
|
|
765
800
|
*/
|
|
766
801
|
export declare const commitTypes: CheckFunction<CommitTypesOptions>;
|
|
767
802
|
//#endregion
|
|
803
|
+
//#region src/checks/derived-types.d.ts
|
|
804
|
+
/**
|
|
805
|
+
* The last names that count as inference when the options name none: those of `command-types`, Drizzle's `InferSelectModel` and `InferInsertModel` generics, and the `$inferSelect` and `$inferInsert` members of a Drizzle table, read as `typeof table.$inferSelect`.
|
|
806
|
+
*/
|
|
807
|
+
export declare const DEFAULT_DERIVED_TYPE_INFERENCES: readonly string[];
|
|
808
|
+
/**
|
|
809
|
+
* For each pair, every exported interface and type alias of its type files is derived from a schema declared in its schema files: somewhere in its definition an inference (a generic in `inferences`, such as `z.infer` or `InferSelectModel`) is applied to `typeof schema`, or an inference member is read from it (`typeof table.$inferSelect`).
|
|
810
|
+
*
|
|
811
|
+
* The definition is followed through aliases and interfaces without type parameters, in any file, and through the arguments of other generics, intersections, unions whose members are all derived (`null` and `undefined` aside), arrays, type operators and indexed access. A type literal, and a schema passed to a generic that is not an inference, are written by hand.
|
|
812
|
+
*/
|
|
813
|
+
export declare const derivedTypes: CheckFunction<DerivedTypesOptions>;
|
|
814
|
+
//#endregion
|
|
768
815
|
//#region src/checks/dockerfile-package-manager.d.ts
|
|
769
816
|
/**
|
|
770
817
|
* Every pin of the package manager that `packageManager` names, in each Dockerfile, is the version `packageManager` names, with any `+sha` integrity suffix ignored on both sides. Pins of other package managers are not the workspace's package manager and are left alone.
|
|
@@ -1275,4 +1322,4 @@ export declare function proveExhaustive(input: {
|
|
|
1275
1322
|
readonly compilerOptions?: CompilerOptions;
|
|
1276
1323
|
}): ExhaustivenessProof;
|
|
1277
1324
|
//#endregion
|
|
1278
|
-
export type { AggregateMappersOptions, BranchRules, CaseRemoval, CheckContext, CheckFunction, CheckName, CheckOptionsByName, CheckResult, ChecksConfig, CommandTypesOptions, CommitTypesOptions, ConformanceConfig, DeployDirectory, DeploySelector, DeployToolAdapter, DeployToolName, DiscoveredPackage, DockerfilePackageManagerOptions, EslintOptions, EslintRequiredSeverity, EslintSample, ExhaustivenessProof, GeneratorAdapter, GeneratorName, GitHubCheckContext, GitHubCheckFunction, GitHubClient, GitHubClientOptions, ImportGraphOptions, InstructionSymlinksOptions, Job, LayoutCheckContext, LayoutCheckFunction, MigrationsDirectoryOptions, PermissionAccess, Permissions, PinningLevel, RegisteredCheck, RepositorySettings, RepositorySlug, RunChecksOptions, RunResult, RunsOn, SettingsMergeMethodsOptions, SettingsOptions, SettingsRequiredChecksOptions, SettingsReviewThreadResolutionOptions, SingleStorybookOptions, SourceLocation, Step, TypeErrorReport, UsesReference, Violation, Workflow, WorkflowActionPinningOptions, WorkflowCredentialsOptions, WorkflowJobOrderingOptions, WorkflowMergeGroupOptions, WorkflowRepositoryDispatchOptions, WorkflowRunnerResolutionOptions, WorkflowSelection, WorkflowSkippableJobsOptions, WorkflowUpdateBotCooldownOptions, WorkflowVersionSingleSourceOptions, WorkspacePackage };
|
|
1325
|
+
export type { AggregateMappersOptions, BranchRules, CaseRemoval, CheckContext, CheckFunction, CheckName, CheckOptionsByName, CheckResult, ChecksConfig, CommandTypesOptions, CommitTypesOptions, ConformanceConfig, DeployDirectory, DeploySelector, DeployToolAdapter, DeployToolName, DerivedTypesOptions, DerivedTypesPair, DiscoveredPackage, DockerfilePackageManagerOptions, EslintOptions, EslintRequiredSeverity, EslintSample, ExhaustivenessProof, GeneratorAdapter, GeneratorName, GitHubCheckContext, GitHubCheckFunction, GitHubClient, GitHubClientOptions, ImportGraphOptions, InstructionSymlinksOptions, Job, LayoutCheckContext, LayoutCheckFunction, MigrationsDirectoryOptions, PermissionAccess, Permissions, PinningLevel, RegisteredCheck, RepositorySettings, RepositorySlug, RunChecksOptions, RunResult, RunsOn, SettingsMergeMethodsOptions, SettingsOptions, SettingsRequiredChecksOptions, SettingsReviewThreadResolutionOptions, SingleStorybookOptions, SourceLocation, Step, TypeErrorReport, UsesReference, Violation, Workflow, WorkflowActionPinningOptions, WorkflowCredentialsOptions, WorkflowJobOrderingOptions, WorkflowMergeGroupOptions, WorkflowRepositoryDispatchOptions, WorkflowRunnerResolutionOptions, WorkflowSelection, WorkflowSkippableJobsOptions, WorkflowUpdateBotCooldownOptions, WorkflowVersionSingleSourceOptions, WorkspacePackage };
|
package/dist/index.js
CHANGED
|
@@ -2,7 +2,7 @@ import { createRequire } from "node:module";
|
|
|
2
2
|
import { existsSync } from "node:fs";
|
|
3
3
|
import { dirname, extname, join, posix, relative, resolve, sep } from "node:path";
|
|
4
4
|
import { escapePath, glob, isDynamicPattern } from "tinyglobby";
|
|
5
|
-
import { ModuleKind, ModuleResolutionKind, Node, Project, ScriptTarget, ts } from "ts-morph";
|
|
5
|
+
import { ModuleKind, ModuleResolutionKind, Node, Project, ScriptTarget, SyntaxKind, ts } from "ts-morph";
|
|
6
6
|
import { createJitiLoader, validateStandard } from "cosmiconfig-extends";
|
|
7
7
|
import { readFile, realpath } from "node:fs/promises";
|
|
8
8
|
import { LineCounter, isMap, isNode, isScalar, isSeq, parse, parseDocument } from "yaml";
|
|
@@ -212,6 +212,32 @@ const aggregateMappers = async ({ cwd, options }) => {
|
|
|
212
212
|
return violations.sort((a, b) => a.file.localeCompare(b.file) || (a.location?.line ?? 0) - (b.location?.line ?? 0) || a.message.localeCompare(b.message));
|
|
213
213
|
};
|
|
214
214
|
//#endregion
|
|
215
|
+
//#region src/checks/type-references.ts
|
|
216
|
+
/**
|
|
217
|
+
* The identifier a reference ends in: `infer` in `z.infer`, the name itself when it is not qualified.
|
|
218
|
+
*/
|
|
219
|
+
function lastIdentifier(reference) {
|
|
220
|
+
if (Node.isQualifiedName(reference)) return reference.getRight();
|
|
221
|
+
if (Node.isPropertyAccessExpression(reference)) return reference.getNameNode();
|
|
222
|
+
return reference;
|
|
223
|
+
}
|
|
224
|
+
/**
|
|
225
|
+
* The declarations the identifier refers to, through any imports and re-exports.
|
|
226
|
+
*/
|
|
227
|
+
function declarationsOf(identifier) {
|
|
228
|
+
const symbol = identifier.getSymbol();
|
|
229
|
+
return (symbol?.getAliasedSymbol() ?? symbol)?.getDeclarations() ?? [];
|
|
230
|
+
}
|
|
231
|
+
/**
|
|
232
|
+
* The names a reference goes by: the last name as written, and the name it was declared with when that differs, as for `Infer` in `import type { infer as Infer } from 'zod'`, which is also `infer`. A library may itself re-export a generic under another name, so both count.
|
|
233
|
+
*/
|
|
234
|
+
function referenceNames(reference) {
|
|
235
|
+
const identifier = lastIdentifier(reference);
|
|
236
|
+
const symbol = identifier.getSymbol();
|
|
237
|
+
const declared = (symbol?.getAliasedSymbol() ?? symbol)?.getName();
|
|
238
|
+
return declared === void 0 ? [identifier.getText()] : [.../* @__PURE__ */ new Set([identifier.getText(), declared])];
|
|
239
|
+
}
|
|
240
|
+
//#endregion
|
|
215
241
|
//#region src/checks/command-types.ts
|
|
216
242
|
/**
|
|
217
243
|
* The last names of the generics that turn a schema into its type, used when the options name none: `z.infer`, `z.input`, `z.output`, `z.TypeOf` and Valibot's `InferInput` and `InferOutput`.
|
|
@@ -229,20 +255,10 @@ const DEFAULT_INFERENCES = [
|
|
|
229
255
|
*/
|
|
230
256
|
const STANDARD_SCHEMA_MEMBER = "~standard";
|
|
231
257
|
/**
|
|
232
|
-
* The names a type reference goes by: the last name as written, and the name it was declared with when that differs, as for `Infer` in `import type { infer as Infer } from 'zod'`, which is also `infer`. A library may itself re-export a generic under another name, so both count.
|
|
233
|
-
*/
|
|
234
|
-
function namesOf(typeName) {
|
|
235
|
-
const identifier = Node.isQualifiedName(typeName) ? typeName.getRight() : typeName;
|
|
236
|
-
const symbol = identifier.getSymbol();
|
|
237
|
-
const declared = (symbol?.getAliasedSymbol() ?? symbol)?.getName();
|
|
238
|
-
return declared === void 0 ? [identifier.getText()] : [.../* @__PURE__ */ new Set([identifier.getText(), declared])];
|
|
239
|
-
}
|
|
240
|
-
/**
|
|
241
258
|
* The local alias without type parameters that a type reference names, or `undefined` when it names anything else. Such an alias is a name for the type it is written as, so it is judged by that.
|
|
242
259
|
*/
|
|
243
260
|
function aliasNamed(typeName) {
|
|
244
|
-
const
|
|
245
|
-
const declaration = (symbol?.getAliasedSymbol() ?? symbol)?.getDeclarations().find(Node.isTypeAliasDeclaration);
|
|
261
|
+
const declaration = declarationsOf(lastIdentifier(typeName)).find(Node.isTypeAliasDeclaration);
|
|
246
262
|
return declaration?.getTypeParameters().length === 0 ? declaration : void 0;
|
|
247
263
|
}
|
|
248
264
|
/**
|
|
@@ -255,7 +271,7 @@ function problem$1(declaration, name, inferences, followed = /* @__PURE__ */ new
|
|
|
255
271
|
};
|
|
256
272
|
const written = declaration.getTypeNode();
|
|
257
273
|
if (Node.isTypeReference(written)) {
|
|
258
|
-
const inference =
|
|
274
|
+
const inference = referenceNames(written.getTypeName()).find((candidate) => inferences.includes(candidate));
|
|
259
275
|
if (inference !== void 0) {
|
|
260
276
|
const [argument] = written.getTypeArguments();
|
|
261
277
|
if (argument !== void 0 && Node.isTypeQuery(argument) && argument.getExprName().getType().getProperty(STANDARD_SCHEMA_MEMBER) !== void 0) return;
|
|
@@ -451,6 +467,120 @@ const commitTypes = async ({ cwd, options, configFiles }) => {
|
|
|
451
467
|
return violations;
|
|
452
468
|
};
|
|
453
469
|
//#endregion
|
|
470
|
+
//#region src/checks/derived-types.ts
|
|
471
|
+
/**
|
|
472
|
+
* The last names that count as inference when the options name none: those of `command-types`, Drizzle's `InferSelectModel` and `InferInsertModel` generics, and the `$inferSelect` and `$inferInsert` members of a Drizzle table, read as `typeof table.$inferSelect`.
|
|
473
|
+
*/
|
|
474
|
+
const DEFAULT_DERIVED_TYPE_INFERENCES = [
|
|
475
|
+
...DEFAULT_INFERENCES,
|
|
476
|
+
"InferSelectModel",
|
|
477
|
+
"InferInsertModel",
|
|
478
|
+
"$inferSelect",
|
|
479
|
+
"$inferInsert"
|
|
480
|
+
];
|
|
481
|
+
/**
|
|
482
|
+
* Whether the node names something declared in one of the schema files.
|
|
483
|
+
*/
|
|
484
|
+
function declaredInSchema(name, derivation) {
|
|
485
|
+
const identifier = Node.isIdentifier(name) ? name : Node.isQualifiedName(name) ? name.getRight() : void 0;
|
|
486
|
+
return identifier !== void 0 && declarationsOf(identifier).some((declaration) => derivation.schemaFiles.has(relativePosix(derivation.cwd, declaration.getSourceFile().getFilePath())));
|
|
487
|
+
}
|
|
488
|
+
/**
|
|
489
|
+
* The interface or type alias without type parameters a reference names, when it has not been followed yet. Such a declaration is a name for what it is written as, so it is judged by that.
|
|
490
|
+
*/
|
|
491
|
+
function followable(reference, derivation) {
|
|
492
|
+
const name = Node.isTypeReference(reference) ? reference.getTypeName() : reference.getExpression();
|
|
493
|
+
if (!Node.isIdentifier(name) && !Node.isQualifiedName(name) && !Node.isPropertyAccessExpression(name)) return;
|
|
494
|
+
const declaration = declarationsOf(lastIdentifier(name)).find((found) => Node.isTypeAliasDeclaration(found) || Node.isInterfaceDeclaration(found));
|
|
495
|
+
return declaration?.getTypeParameters().length === 0 && !derivation.followed.has(declaration) ? declaration : void 0;
|
|
496
|
+
}
|
|
497
|
+
function isNullish(node) {
|
|
498
|
+
return node.getKind() === SyntaxKind.UndefinedKeyword || Node.isLiteralTypeNode(node) && node.getLiteral().getKind() === SyntaxKind.NullKeyword;
|
|
499
|
+
}
|
|
500
|
+
/**
|
|
501
|
+
* Whether a reference is derived: an inference applied to `typeof schema`, an alias or interface that is itself derived, or another generic given a derived type, as `Partial<Order>` is.
|
|
502
|
+
*/
|
|
503
|
+
function derivedReference(reference, derivation) {
|
|
504
|
+
const name = Node.isTypeReference(reference) ? reference.getTypeName() : reference.getExpression();
|
|
505
|
+
if ((Node.isIdentifier(name) || Node.isQualifiedName(name) || Node.isPropertyAccessExpression(name)) && referenceNames(name).some((candidate) => derivation.inferences.has(candidate))) {
|
|
506
|
+
const [argument] = reference.getTypeArguments();
|
|
507
|
+
return argument !== void 0 && Node.isTypeQuery(argument) && declaredInSchema(argument.getExprName(), derivation);
|
|
508
|
+
}
|
|
509
|
+
const declaration = followable(reference, derivation);
|
|
510
|
+
if (declaration !== void 0) return derivedDeclaration(declaration, {
|
|
511
|
+
...derivation,
|
|
512
|
+
followed: /* @__PURE__ */ new Set([...derivation.followed, declaration])
|
|
513
|
+
});
|
|
514
|
+
return reference.getTypeArguments().some((argument) => derivedType(argument, derivation));
|
|
515
|
+
}
|
|
516
|
+
/**
|
|
517
|
+
* Whether a type, as written, is derived from a schema. A type literal is written by hand whatever its members are.
|
|
518
|
+
*/
|
|
519
|
+
function derivedType(node, derivation) {
|
|
520
|
+
if (Node.isTypeReference(node) || Node.isExpressionWithTypeArguments(node)) return derivedReference(node, derivation);
|
|
521
|
+
if (Node.isTypeQuery(node)) {
|
|
522
|
+
const name = node.getExprName();
|
|
523
|
+
return Node.isQualifiedName(name) && referenceNames(name).some((candidate) => derivation.inferences.has(candidate)) && declaredInSchema(name.getLeft(), derivation);
|
|
524
|
+
}
|
|
525
|
+
if (Node.isParenthesizedTypeNode(node) || Node.isTypeOperatorTypeNode(node)) return derivedType(node.getTypeNode(), derivation);
|
|
526
|
+
if (Node.isArrayTypeNode(node)) return derivedType(node.getElementTypeNode(), derivation);
|
|
527
|
+
if (Node.isIndexedAccessTypeNode(node)) return derivedType(node.getObjectTypeNode(), derivation);
|
|
528
|
+
if (Node.isIntersectionTypeNode(node)) return node.getTypeNodes().some((member) => derivedType(member, derivation));
|
|
529
|
+
if (Node.isUnionTypeNode(node)) {
|
|
530
|
+
const members = node.getTypeNodes().filter((member) => !isNullish(member));
|
|
531
|
+
return members.length > 0 && members.every((member) => derivedType(member, derivation));
|
|
532
|
+
}
|
|
533
|
+
return false;
|
|
534
|
+
}
|
|
535
|
+
/**
|
|
536
|
+
* Whether an interface or type alias is derived: an alias by the type it is written as, an interface by whether it extends a derived type.
|
|
537
|
+
*/
|
|
538
|
+
function derivedDeclaration(declaration, derivation) {
|
|
539
|
+
if (Node.isInterfaceDeclaration(declaration)) return declaration.getExtends().some((heritage) => derivedReference(heritage, derivation));
|
|
540
|
+
return derivedType(declaration.getTypeNodeOrThrow(), derivation);
|
|
541
|
+
}
|
|
542
|
+
async function pairViolations(cwd, options, pair, index) {
|
|
543
|
+
const { project, files } = await createProject(cwd, options.tsConfig, pair.types, {
|
|
544
|
+
check: "derived-types",
|
|
545
|
+
files: `pairs.${String(index)}.types`
|
|
546
|
+
});
|
|
547
|
+
const schemaFiles = await findFiles(cwd, pair.schemas);
|
|
548
|
+
if (schemaFiles.length === 0) throw new ConformanceError(`checks.derived-types.pairs.${String(index)}.schemas: no file matches`);
|
|
549
|
+
const excluded = new Set(options.exclude);
|
|
550
|
+
const derivation = {
|
|
551
|
+
inferences: new Set(options.inferences ?? DEFAULT_DERIVED_TYPE_INFERENCES),
|
|
552
|
+
schemaFiles: new Set(schemaFiles),
|
|
553
|
+
cwd,
|
|
554
|
+
followed: /* @__PURE__ */ new Set()
|
|
555
|
+
};
|
|
556
|
+
const seen = /* @__PURE__ */ new Set();
|
|
557
|
+
const violations = [];
|
|
558
|
+
for (const file of files) for (const [name, declaration] of exportedTypes(project.getSourceFileOrThrow(resolve(cwd, file)))) {
|
|
559
|
+
if (excluded.has(name) || seen.has(declaration)) continue;
|
|
560
|
+
seen.add(declaration);
|
|
561
|
+
if (!derivedDeclaration(declaration, {
|
|
562
|
+
...derivation,
|
|
563
|
+
followed: /* @__PURE__ */ new Set([declaration])
|
|
564
|
+
})) violations.push({
|
|
565
|
+
code: "derived-types/not-derived",
|
|
566
|
+
message: `${name} is not derived from a schema in ${pair.schemas.join(", ")}: nothing in its definition applies an inference generic to one`,
|
|
567
|
+
file: relativePosix(cwd, declaration.getSourceFile().getFilePath()),
|
|
568
|
+
location: declarationLocation(declaration)
|
|
569
|
+
});
|
|
570
|
+
}
|
|
571
|
+
return violations;
|
|
572
|
+
}
|
|
573
|
+
/**
|
|
574
|
+
* For each pair, every exported interface and type alias of its type files is derived from a schema declared in its schema files: somewhere in its definition an inference (a generic in `inferences`, such as `z.infer` or `InferSelectModel`) is applied to `typeof schema`, or an inference member is read from it (`typeof table.$inferSelect`).
|
|
575
|
+
*
|
|
576
|
+
* The definition is followed through aliases and interfaces without type parameters, in any file, and through the arguments of other generics, intersections, unions whose members are all derived (`null` and `undefined` aside), arrays, type operators and indexed access. A type literal, and a schema passed to a generic that is not an inference, are written by hand.
|
|
577
|
+
*/
|
|
578
|
+
const derivedTypes = async ({ cwd, options }) => {
|
|
579
|
+
const violations = [];
|
|
580
|
+
for (const [index, pair] of options.pairs.entries()) violations.push(...await pairViolations(cwd, options, pair, index));
|
|
581
|
+
return violations.sort((a, b) => a.file.localeCompare(b.file) || (a.location?.line ?? 0) - (b.location?.line ?? 0) || a.message.localeCompare(b.message));
|
|
582
|
+
};
|
|
583
|
+
//#endregion
|
|
454
584
|
//#region src/checks/dockerfile-package-manager.ts
|
|
455
585
|
const DEFAULT_DOCKERFILES = [
|
|
456
586
|
"**/Dockerfile",
|
|
@@ -2978,6 +3108,15 @@ const checks = z.strictObject({
|
|
|
2978
3108
|
inferences: z.exactOptional(paths),
|
|
2979
3109
|
tsConfig: z.exactOptional(path)
|
|
2980
3110
|
}))),
|
|
3111
|
+
"derived-types": z.exactOptional(enabled(z.strictObject({
|
|
3112
|
+
pairs: z.array(z.strictObject({
|
|
3113
|
+
schemas: paths,
|
|
3114
|
+
types: paths
|
|
3115
|
+
})).min(1),
|
|
3116
|
+
exclude: z.exactOptional(names),
|
|
3117
|
+
inferences: z.exactOptional(paths),
|
|
3118
|
+
tsConfig: z.exactOptional(path)
|
|
3119
|
+
}))),
|
|
2981
3120
|
"import-uphill": z.exactOptional(enabled(importGraph)),
|
|
2982
3121
|
"import-rank-skip": z.exactOptional(enabled(importGraph)),
|
|
2983
3122
|
"import-cross-slice": z.exactOptional(enabled(importGraph)),
|
|
@@ -3225,6 +3364,12 @@ const registry = {
|
|
|
3225
3364
|
select: (checks) => enabledOptions(checks["command-types"]),
|
|
3226
3365
|
check: commandTypes
|
|
3227
3366
|
}),
|
|
3367
|
+
"derived-types": plain({
|
|
3368
|
+
name: "derived-types",
|
|
3369
|
+
description: "Every exported type in a file paired with schema files is derived from one of those schemas, not hand-written",
|
|
3370
|
+
select: (checks) => enabledOptions(checks["derived-types"]),
|
|
3371
|
+
check: derivedTypes
|
|
3372
|
+
}),
|
|
3228
3373
|
"import-uphill": importCheck({
|
|
3229
3374
|
name: "import-uphill",
|
|
3230
3375
|
description: "No package imports a package of a higher rank",
|
|
@@ -3572,4 +3717,4 @@ function proveExhaustive(input) {
|
|
|
3572
3717
|
}) };
|
|
3573
3718
|
}
|
|
3574
3719
|
//#endregion
|
|
3575
|
-
export { ConformanceError, DEFAULT_INFERENCES, DEFAULT_WORKFLOWS, EXIT_CODES, ExhaustivenessError, PRESET_COMMIT_TYPES, accessOf, aggregateMappers, checkNames, classifyPackages, commandTypes, commitTypes, conformanceSchema, conformanceSection, createGitHubClient, crossSliceRules, cycleRule, deployTools, dockerfilePackageManager, effectiveEnv, effectivePermissions, eslint, generators, handlerCases, importCrossSlice, importCycles, importIsolatedGroups, importRankSkip, importUphill, instructionSymlinks, isCheckName, isolatedGroupRules, loadWorkflows, migrationsDirectory, packagesPattern, parseUses, parseWorkflow, proveExhaustive, rankSkipRules, registry, removeCase, runChecks, settingsMergeMethods, settingsRequiredChecks, settingsReviewThreadResolution, singleStorybook, transitiveNeeds, typeErrors, uphillRules, workflowActionPinning, workflowCredentials, workflowJobOrdering, workflowMergeGroup, workflowRepositoryDispatch, workflowRunnerResolution, workflowSkippableJobs, workflowUpdateBotCooldown, workflowVersionSingleSource };
|
|
3720
|
+
export { ConformanceError, DEFAULT_DERIVED_TYPE_INFERENCES, DEFAULT_INFERENCES, DEFAULT_WORKFLOWS, EXIT_CODES, ExhaustivenessError, PRESET_COMMIT_TYPES, accessOf, aggregateMappers, checkNames, classifyPackages, commandTypes, commitTypes, conformanceSchema, conformanceSection, createGitHubClient, crossSliceRules, cycleRule, deployTools, derivedTypes, dockerfilePackageManager, effectiveEnv, effectivePermissions, eslint, generators, handlerCases, importCrossSlice, importCycles, importIsolatedGroups, importRankSkip, importUphill, instructionSymlinks, isCheckName, isolatedGroupRules, loadWorkflows, migrationsDirectory, packagesPattern, parseUses, parseWorkflow, proveExhaustive, rankSkipRules, registry, removeCase, runChecks, settingsMergeMethods, settingsRequiredChecks, settingsReviewThreadResolution, singleStorybook, transitiveNeeds, typeErrors, uphillRules, workflowActionPinning, workflowCredentials, workflowJobOrdering, workflowMergeGroup, workflowRepositoryDispatch, workflowRunnerResolution, workflowSkippableJobs, workflowUpdateBotCooldown, workflowVersionSingleSource };
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "workspace-conformance",
|
|
3
3
|
"description": "Conformance checks for a pnpm workspace that ESLint cannot express: type-graph, import-graph and cross-file checks driven by the shared layout section of @exadev/config",
|
|
4
|
-
"version": "1.
|
|
4
|
+
"version": "1.4.0",
|
|
5
5
|
"bin": {
|
|
6
6
|
"workspace-conformance": "./dist/cli.js"
|
|
7
7
|
},
|