@ontrails/warden 1.0.0-beta.4 → 1.0.0-beta.41
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +835 -13
- package/README.md +133 -26
- package/bin/warden.ts +51 -0
- package/package.json +27 -6
- package/src/adapter-check.ts +136 -0
- package/src/cli.ts +1570 -105
- package/src/command.ts +986 -0
- package/src/config.ts +193 -0
- package/src/draft.ts +22 -0
- package/src/drift.ts +233 -23
- package/src/fix.ts +126 -0
- package/src/formatters.ts +78 -13
- package/src/guide.ts +245 -0
- package/src/index.ts +247 -15
- package/src/project-context.ts +446 -0
- package/src/project-rules.ts +290 -0
- package/src/resolve.ts +531 -0
- package/src/rules/activation-orphan.ts +97 -0
- package/src/rules/captured-kernel.ts +375 -0
- package/src/rules/circular-refs.ts +150 -0
- package/src/rules/cli-command-route-coherence.ts +177 -0
- package/src/rules/composes-declarations.ts +839 -0
- package/src/rules/context-no-surface-types.ts +79 -15
- package/src/rules/dead-internal-trail.ts +161 -0
- package/src/rules/dead-public-trail.ts +258 -0
- package/src/rules/draft-file-marking.ts +155 -0
- package/src/rules/draft-visible-debt.ts +82 -0
- package/src/rules/duplicate-exported-symbol.ts +211 -0
- package/src/rules/duplicate-public-contract.ts +137 -0
- package/src/rules/entity-exists.ts +254 -0
- package/src/rules/entity-ids.ts +15 -0
- package/src/rules/error-mapping-completeness.ts +290 -0
- package/src/rules/example-valid.ts +395 -0
- package/src/rules/fires-declarations.ts +740 -0
- package/src/rules/governed-symbol-residue.ts +438 -0
- package/src/rules/implementation-returns-result.ts +1409 -166
- package/src/rules/incomplete-accessor-for-standard-op.ts +272 -0
- package/src/rules/incomplete-crud.ts +583 -0
- package/src/rules/index.ts +277 -10
- package/src/rules/intent-propagation.ts +125 -0
- package/src/rules/layer-field-name-drift.ts +102 -0
- package/src/rules/library-projection-coherence.ts +100 -0
- package/src/rules/metadata.ts +871 -0
- package/src/rules/missing-reconcile.ts +97 -0
- package/src/rules/missing-visibility.ts +111 -0
- package/src/rules/no-destructured-compose.ts +196 -0
- package/src/rules/no-dev-permit-in-source.ts +99 -0
- package/src/rules/no-direct-implementation-call.ts +12 -7
- package/src/rules/no-legacy-cli-alias-export.ts +247 -0
- package/src/rules/no-legacy-layer-imports.ts +211 -0
- package/src/rules/no-native-error-result.ts +118 -0
- package/src/rules/no-redundant-result-error-wrap.ts +384 -0
- package/src/rules/no-retired-cross-vocabulary.ts +204 -0
- package/src/rules/no-sync-result-assumption.ts +1141 -98
- package/src/rules/no-throw-in-detour-recover.ts +225 -0
- package/src/rules/no-throw-in-implementation.ts +15 -8
- package/src/rules/no-top-level-surface.ts +371 -0
- package/src/rules/on-references-exist.ts +194 -0
- package/src/rules/orphaned-signal.ts +149 -0
- package/src/rules/owner-projection-parity.ts +143 -0
- package/src/rules/permit-governance.ts +25 -0
- package/src/rules/public-export-example-coverage.ts +573 -0
- package/src/rules/public-internal-deep-imports.ts +456 -0
- package/src/rules/public-output-schema.ts +29 -0
- package/src/rules/public-union-output-discriminants.ts +150 -0
- package/src/rules/read-intent-fires.ts +188 -0
- package/src/rules/reference-exists.ts +97 -0
- package/src/rules/registry-names.ts +167 -0
- package/src/rules/resolved-import-boundary.ts +146 -0
- package/src/rules/resource-declarations.ts +697 -0
- package/src/rules/resource-exists.ts +181 -0
- package/src/rules/resource-id-grammar.ts +65 -0
- package/src/rules/resource-mock-coverage.ts +115 -0
- package/src/rules/retired-vocabulary.ts +633 -0
- package/src/rules/scan.ts +38 -25
- package/src/rules/scheduled-destroy-intent.ts +44 -0
- package/src/rules/signal-graph-coaching.ts +220 -0
- package/src/rules/source/composition.ts +165 -0
- package/src/rules/source/drafts.ts +164 -0
- package/src/rules/source/entities.ts +618 -0
- package/src/rules/source/pragmas.ts +45 -0
- package/src/rules/source/resources.ts +64 -0
- package/src/rules/source/signals.ts +397 -0
- package/src/rules/source/stores.ts +310 -0
- package/src/rules/specs.ts +9 -5
- package/src/rules/static-resource-accessor-preference.ts +653 -0
- package/src/rules/surface-overlay-coherence.ts +262 -0
- package/src/rules/surface-trailhead-coherence.ts +366 -0
- package/src/rules/trail-fork-coaching.ts +625 -0
- package/src/rules/trail-versioning-source.ts +1076 -0
- package/src/rules/trail-versioning-topo.ts +172 -0
- package/src/rules/trailhead-override-divergence.ts +356 -0
- package/src/rules/types.ts +354 -8
- package/src/rules/unmaterialized-activation-source.ts +85 -0
- package/src/rules/unreachable-detour-shadowing.ts +339 -0
- package/src/rules/valid-describe-refs.ts +162 -32
- package/src/rules/valid-detour-contract.ts +78 -0
- package/src/rules/warden-export-symmetry.ts +540 -0
- package/src/rules/warden-rules-use-ast.ts +1109 -0
- package/src/rules/webhook-route-collision.ts +306 -0
- package/src/trails/activation-orphan.trail.ts +84 -0
- package/src/trails/captured-kernel.trail.ts +108 -0
- package/src/trails/circular-refs.trail.ts +29 -0
- package/src/trails/cli-command-route-coherence.trail.ts +47 -0
- package/src/trails/composes-declarations.trail.ts +22 -0
- package/src/trails/context-no-surface-types.trail.ts +21 -0
- package/src/trails/dead-internal-trail.trail.ts +26 -0
- package/src/trails/dead-public-trail.trail.ts +31 -0
- package/src/trails/deprecation-without-guidance.trail.ts +21 -0
- package/src/trails/draft-file-marking.trail.ts +16 -0
- package/src/trails/draft-visible-debt.trail.ts +16 -0
- package/src/trails/duplicate-exported-symbol.trail.ts +48 -0
- package/src/trails/duplicate-public-contract.trail.ts +47 -0
- package/src/trails/entity-exists.trail.ts +21 -0
- package/src/trails/error-mapping-completeness.trail.ts +30 -0
- package/src/trails/example-valid.trail.ts +25 -0
- package/src/trails/fires-declarations.trail.ts +23 -0
- package/src/trails/fork-without-preserved-implementation.trail.ts +31 -0
- package/src/trails/governed-symbol-residue.trail.ts +24 -0
- package/src/trails/implementation-returns-result.trail.ts +20 -0
- package/src/trails/incomplete-accessor-for-standard-op.trail.ts +76 -0
- package/src/trails/incomplete-crud.trail.ts +39 -0
- package/src/trails/index.ts +89 -0
- package/src/trails/intent-propagation.trail.ts +30 -0
- package/src/trails/layer-field-name-drift.trail.ts +39 -0
- package/src/trails/library-projection-coherence.trail.ts +43 -0
- package/src/trails/marker-schema-unsupported.trail.ts +23 -0
- package/src/trails/missing-reconcile.trail.ts +33 -0
- package/src/trails/missing-visibility.trail.ts +22 -0
- package/src/trails/no-destructured-compose.trail.ts +44 -0
- package/src/trails/no-dev-permit-in-source.trail.ts +16 -0
- package/src/trails/no-direct-implementation-call.trail.ts +16 -0
- package/src/trails/no-legacy-cli-alias-export.trail.ts +41 -0
- package/src/trails/no-legacy-layer-imports.trail.ts +41 -0
- package/src/trails/no-native-error-result.trail.ts +18 -0
- package/src/trails/no-redundant-result-error-wrap.trail.ts +55 -0
- package/src/trails/no-retired-cross-vocabulary.trail.ts +42 -0
- package/src/trails/no-sync-result-assumption.trail.ts +19 -0
- package/src/trails/no-throw-in-detour-recover.trail.ts +24 -0
- package/src/trails/no-throw-in-implementation.trail.ts +20 -0
- package/src/trails/no-top-level-surface.trail.ts +43 -0
- package/src/trails/on-references-exist.trail.ts +21 -0
- package/src/trails/orphaned-signal.trail.ts +36 -0
- package/src/trails/owner-projection-parity.trail.ts +26 -0
- package/src/trails/pending-force.trail.ts +21 -0
- package/src/trails/permit-governance.trail.ts +51 -0
- package/src/trails/prefer-schema-inference.trail.ts +21 -0
- package/src/trails/public-export-example-coverage.trail.ts +16 -0
- package/src/trails/public-internal-deep-imports.trail.ts +94 -0
- package/src/trails/public-output-schema.trail.ts +55 -0
- package/src/trails/public-union-output-discriminants.trail.ts +33 -0
- package/src/trails/read-intent-fires.trail.ts +20 -0
- package/src/trails/reference-exists.trail.ts +25 -0
- package/src/trails/resolved-import-boundary.trail.ts +109 -0
- package/src/trails/resource-declarations.trail.ts +25 -0
- package/src/trails/resource-exists.trail.ts +27 -0
- package/src/trails/resource-id-grammar.trail.ts +39 -0
- package/src/trails/resource-mock-coverage.trail.ts +40 -0
- package/src/trails/run.ts +160 -0
- package/src/trails/scheduled-destroy-intent.trail.ts +56 -0
- package/src/trails/schema.ts +237 -0
- package/src/trails/signal-graph-coaching.trail.ts +77 -0
- package/src/trails/static-resource-accessor-preference.trail.ts +25 -0
- package/src/trails/surface-overlay-coherence.trail.ts +24 -0
- package/src/trails/surface-trailhead-coherence.trail.ts +25 -0
- package/src/trails/topo.ts +6 -0
- package/src/trails/trail-fork-coaching.trail.ts +42 -0
- package/src/trails/trailhead-override-divergence.trail.ts +47 -0
- package/src/trails/unmaterialized-activation-source.trail.ts +72 -0
- package/src/trails/unreachable-detour-shadowing.trail.ts +45 -0
- package/src/trails/valid-describe-refs.trail.ts +18 -0
- package/src/trails/valid-detour-contract.trail.ts +71 -0
- package/src/trails/version-gap.trail.ts +35 -0
- package/src/trails/version-pinned-compose.trail.ts +23 -0
- package/src/trails/version-without-examples.trail.ts +38 -0
- package/src/trails/warden-export-symmetry.trail.ts +16 -0
- package/src/trails/warden-rules-use-ast.trail.ts +64 -0
- package/src/trails/webhook-route-collision.trail.ts +50 -0
- package/src/trails/wrap-rule.ts +224 -0
- package/src/workspaces.ts +199 -0
- package/.turbo/turbo-build.log +0 -1
- package/.turbo/turbo-lint.log +0 -3
- package/.turbo/turbo-typecheck.log +0 -1
- package/dist/cli.d.ts +0 -46
- package/dist/cli.d.ts.map +0 -1
- package/dist/cli.js +0 -218
- package/dist/cli.js.map +0 -1
- package/dist/drift.d.ts +0 -26
- package/dist/drift.d.ts.map +0 -1
- package/dist/drift.js +0 -27
- package/dist/drift.js.map +0 -1
- package/dist/formatters.d.ts +0 -29
- package/dist/formatters.d.ts.map +0 -1
- package/dist/formatters.js +0 -87
- package/dist/formatters.js.map +0 -1
- package/dist/index.d.ts +0 -26
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js +0 -26
- package/dist/index.js.map +0 -1
- package/dist/rules/ast.d.ts +0 -41
- package/dist/rules/ast.d.ts.map +0 -1
- package/dist/rules/ast.js +0 -161
- package/dist/rules/ast.js.map +0 -1
- package/dist/rules/context-no-surface-types.d.ts +0 -12
- package/dist/rules/context-no-surface-types.d.ts.map +0 -1
- package/dist/rules/context-no-surface-types.js +0 -96
- package/dist/rules/context-no-surface-types.js.map +0 -1
- package/dist/rules/implementation-returns-result.d.ts +0 -13
- package/dist/rules/implementation-returns-result.d.ts.map +0 -1
- package/dist/rules/implementation-returns-result.js +0 -277
- package/dist/rules/implementation-returns-result.js.map +0 -1
- package/dist/rules/index.d.ts +0 -15
- package/dist/rules/index.d.ts.map +0 -1
- package/dist/rules/index.js +0 -34
- package/dist/rules/index.js.map +0 -1
- package/dist/rules/no-direct-impl-in-route.d.ts +0 -12
- package/dist/rules/no-direct-impl-in-route.d.ts.map +0 -1
- package/dist/rules/no-direct-impl-in-route.js +0 -47
- package/dist/rules/no-direct-impl-in-route.js.map +0 -1
- package/dist/rules/no-direct-implementation-call.d.ts +0 -12
- package/dist/rules/no-direct-implementation-call.d.ts.map +0 -1
- package/dist/rules/no-direct-implementation-call.js +0 -39
- package/dist/rules/no-direct-implementation-call.js.map +0 -1
- package/dist/rules/no-sync-result-assumption.d.ts +0 -6
- package/dist/rules/no-sync-result-assumption.d.ts.map +0 -1
- package/dist/rules/no-sync-result-assumption.js +0 -98
- package/dist/rules/no-sync-result-assumption.js.map +0 -1
- package/dist/rules/no-throw-in-detour-target.d.ts +0 -12
- package/dist/rules/no-throw-in-detour-target.d.ts.map +0 -1
- package/dist/rules/no-throw-in-detour-target.js +0 -87
- package/dist/rules/no-throw-in-detour-target.js.map +0 -1
- package/dist/rules/no-throw-in-implementation.d.ts +0 -9
- package/dist/rules/no-throw-in-implementation.d.ts.map +0 -1
- package/dist/rules/no-throw-in-implementation.js +0 -34
- package/dist/rules/no-throw-in-implementation.js.map +0 -1
- package/dist/rules/prefer-schema-inference.d.ts +0 -7
- package/dist/rules/prefer-schema-inference.d.ts.map +0 -1
- package/dist/rules/prefer-schema-inference.js +0 -86
- package/dist/rules/prefer-schema-inference.js.map +0 -1
- package/dist/rules/scan.d.ts +0 -8
- package/dist/rules/scan.d.ts.map +0 -1
- package/dist/rules/scan.js +0 -32
- package/dist/rules/scan.js.map +0 -1
- package/dist/rules/specs.d.ts +0 -29
- package/dist/rules/specs.d.ts.map +0 -1
- package/dist/rules/specs.js +0 -192
- package/dist/rules/specs.js.map +0 -1
- package/dist/rules/structure.d.ts +0 -13
- package/dist/rules/structure.d.ts.map +0 -1
- package/dist/rules/structure.js +0 -142
- package/dist/rules/structure.js.map +0 -1
- package/dist/rules/types.d.ts +0 -52
- package/dist/rules/types.d.ts.map +0 -1
- package/dist/rules/types.js +0 -2
- package/dist/rules/types.js.map +0 -1
- package/dist/rules/valid-describe-refs.d.ts +0 -7
- package/dist/rules/valid-describe-refs.d.ts.map +0 -1
- package/dist/rules/valid-describe-refs.js +0 -51
- package/dist/rules/valid-describe-refs.js.map +0 -1
- package/dist/rules/valid-detour-refs.d.ts +0 -6
- package/dist/rules/valid-detour-refs.d.ts.map +0 -1
- package/dist/rules/valid-detour-refs.js +0 -116
- package/dist/rules/valid-detour-refs.js.map +0 -1
- package/src/__tests__/cli.test.ts +0 -198
- package/src/__tests__/drift.test.ts +0 -74
- package/src/__tests__/formatters.test.ts +0 -157
- package/src/__tests__/implementation-returns-result.test.ts +0 -129
- package/src/__tests__/no-direct-implementation-call.test.ts +0 -83
- package/src/__tests__/no-sync-result-assumption.test.ts +0 -85
- package/src/__tests__/no-throw-in-detour-target.test.ts +0 -78
- package/src/__tests__/prefer-schema-inference.test.ts +0 -84
- package/src/__tests__/rules.test.ts +0 -227
- package/src/__tests__/valid-describe-refs.test.ts +0 -60
- package/src/rules/ast.ts +0 -213
- package/src/rules/no-direct-impl-in-route.ts +0 -81
- package/src/rules/no-throw-in-detour-target.ts +0 -150
- package/src/rules/valid-detour-refs.ts +0 -187
- package/tsconfig.json +0 -9
- package/tsconfig.tsbuildinfo +0 -1
|
@@ -0,0 +1,395 @@
|
|
|
1
|
+
import { entity } from '@ontrails/core';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
extractStringLiteral,
|
|
6
|
+
findConfigProperty,
|
|
7
|
+
findEntityDefinitions,
|
|
8
|
+
getNodeExpression,
|
|
9
|
+
getNodeObject,
|
|
10
|
+
getNodeProperty,
|
|
11
|
+
getStringValue,
|
|
12
|
+
identifierName,
|
|
13
|
+
offsetToLine,
|
|
14
|
+
parse,
|
|
15
|
+
} from '@ontrails/source';
|
|
16
|
+
import type { AstNode, EntityDefinition } from '@ontrails/source';
|
|
17
|
+
import { isTestFile } from './scan.js';
|
|
18
|
+
import type { WardenDiagnostic, WardenRule } from './types.js';
|
|
19
|
+
|
|
20
|
+
class UnsupportedEntityEvaluationError extends Error {}
|
|
21
|
+
|
|
22
|
+
type EntityEvaluationEnvironment = ReadonlyMap<string, unknown>;
|
|
23
|
+
|
|
24
|
+
const buildInvalidExampleDiagnostic = (
|
|
25
|
+
entityName: string,
|
|
26
|
+
message: string,
|
|
27
|
+
filePath: string,
|
|
28
|
+
line: number
|
|
29
|
+
): WardenDiagnostic => ({
|
|
30
|
+
filePath,
|
|
31
|
+
line,
|
|
32
|
+
message: `Entity "${entityName}" has invalid examples: ${message}`,
|
|
33
|
+
rule: 'example-valid',
|
|
34
|
+
severity: 'error',
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
const getPropertyName = (node: unknown): string | null => {
|
|
38
|
+
if (typeof node !== 'object' || node === null) {
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const { name } = node as { readonly name?: unknown };
|
|
43
|
+
if (typeof name === 'string') {
|
|
44
|
+
return name;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return extractStringLiteral(node as AstNode);
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const requireNode = (node: AstNode | undefined): AstNode => {
|
|
51
|
+
if (!node) {
|
|
52
|
+
throw new UnsupportedEntityEvaluationError(
|
|
53
|
+
'Missing node in entity evaluation.'
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return node;
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
type EntityNodeEvaluator = (
|
|
61
|
+
node: AstNode,
|
|
62
|
+
env: EntityEvaluationEnvironment
|
|
63
|
+
) => unknown;
|
|
64
|
+
|
|
65
|
+
// The evaluator table is declared before `evaluateNode` so the recursive
|
|
66
|
+
// dispatch can read it at call time without any forward references. Entries
|
|
67
|
+
// are registered below once each per-type evaluator is defined.
|
|
68
|
+
const entityNodeEvaluators = new Map<string, EntityNodeEvaluator>();
|
|
69
|
+
|
|
70
|
+
const evaluateNode: EntityNodeEvaluator = (
|
|
71
|
+
node: AstNode,
|
|
72
|
+
env: EntityEvaluationEnvironment
|
|
73
|
+
): unknown => {
|
|
74
|
+
const evaluator = entityNodeEvaluators.get(node.type);
|
|
75
|
+
if (evaluator) {
|
|
76
|
+
return evaluator(node, env);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
throw new UnsupportedEntityEvaluationError(
|
|
80
|
+
`Unsupported AST node "${node.type}" in entity evaluation.`
|
|
81
|
+
);
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
const evaluateArrayExpression: EntityNodeEvaluator = (
|
|
85
|
+
node: AstNode,
|
|
86
|
+
env: EntityEvaluationEnvironment
|
|
87
|
+
): readonly unknown[] => {
|
|
88
|
+
const elements = node['elements'] as readonly AstNode[] | undefined;
|
|
89
|
+
return (elements ?? []).map((element) => evaluateNode(element, env));
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
const evaluateLiteralExpression: EntityNodeEvaluator = (
|
|
93
|
+
node: AstNode
|
|
94
|
+
): unknown => getStringValue(node) ?? node.value;
|
|
95
|
+
|
|
96
|
+
const evaluateIdentifierExpression: EntityNodeEvaluator = (
|
|
97
|
+
node: AstNode,
|
|
98
|
+
env: EntityEvaluationEnvironment
|
|
99
|
+
): unknown => {
|
|
100
|
+
const name = identifierName(node);
|
|
101
|
+
if (name === 'undefined') {
|
|
102
|
+
return undefined;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (name === 'z') {
|
|
106
|
+
return z;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (!name || !env.has(name)) {
|
|
110
|
+
throw new UnsupportedEntityEvaluationError(
|
|
111
|
+
`Unknown identifier "${name ?? '<unknown>'}" in entity evaluation.`
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return env.get(name);
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
const evaluateWrappedExpression: EntityNodeEvaluator = (
|
|
119
|
+
node: AstNode,
|
|
120
|
+
env: EntityEvaluationEnvironment
|
|
121
|
+
): unknown => evaluateNode(requireNode(getNodeExpression(node)), env);
|
|
122
|
+
|
|
123
|
+
const evaluateNullExpression: EntityNodeEvaluator = (): null => null;
|
|
124
|
+
|
|
125
|
+
const evaluateObjectExpression: EntityNodeEvaluator = (
|
|
126
|
+
node: AstNode,
|
|
127
|
+
env: EntityEvaluationEnvironment
|
|
128
|
+
): Record<string, unknown> => {
|
|
129
|
+
const properties = node['properties'] as readonly AstNode[] | undefined;
|
|
130
|
+
const value: Record<string, unknown> = {};
|
|
131
|
+
|
|
132
|
+
for (const property of properties ?? []) {
|
|
133
|
+
if (property.type !== 'Property') {
|
|
134
|
+
throw new UnsupportedEntityEvaluationError(
|
|
135
|
+
`Unsupported object property type "${property.type}".`
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const propertyName = getPropertyName(property.key);
|
|
140
|
+
if (!propertyName) {
|
|
141
|
+
throw new UnsupportedEntityEvaluationError(
|
|
142
|
+
'Unsupported object property key in entity evaluation.'
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
value[propertyName] = evaluateNode(
|
|
147
|
+
requireNode(property.value as AstNode | undefined),
|
|
148
|
+
env
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return value;
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
const evaluateCallArguments = (
|
|
156
|
+
node: AstNode,
|
|
157
|
+
env: EntityEvaluationEnvironment
|
|
158
|
+
): readonly unknown[] =>
|
|
159
|
+
((node['arguments'] as readonly AstNode[] | undefined) ?? []).map((arg) =>
|
|
160
|
+
evaluateNode(arg, env)
|
|
161
|
+
);
|
|
162
|
+
|
|
163
|
+
const resolveEntityFallbackOptions = (
|
|
164
|
+
shape: unknown
|
|
165
|
+
): Parameters<typeof entity>[2] | null =>
|
|
166
|
+
typeof shape === 'object' &&
|
|
167
|
+
shape !== null &&
|
|
168
|
+
Object.hasOwn(shape, 'id') &&
|
|
169
|
+
(shape as z.ZodRawShape)['id'] !== undefined
|
|
170
|
+
? ({ identity: 'id' } as Parameters<typeof entity>[2])
|
|
171
|
+
: null;
|
|
172
|
+
|
|
173
|
+
const resolveEntityOptions = (
|
|
174
|
+
shape: unknown,
|
|
175
|
+
options: unknown
|
|
176
|
+
): Parameters<typeof entity>[2] => {
|
|
177
|
+
if (options === undefined) {
|
|
178
|
+
const fallbackOptions = resolveEntityFallbackOptions(shape);
|
|
179
|
+
if (fallbackOptions !== null) {
|
|
180
|
+
return fallbackOptions;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
throw new UnsupportedEntityEvaluationError(
|
|
184
|
+
'Entity evaluator requires literal options, or an `id` field when options are omitted.'
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
if (typeof options !== 'object' || options === null) {
|
|
189
|
+
throw new UnsupportedEntityEvaluationError(
|
|
190
|
+
'Entity evaluator requires literal options when provided.'
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
return options as Parameters<typeof entity>[2];
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
const evaluateEntityCall = (args: readonly unknown[]): unknown => {
|
|
198
|
+
const [name, shape, options] = args;
|
|
199
|
+
if (typeof name !== 'string') {
|
|
200
|
+
throw new UnsupportedEntityEvaluationError(
|
|
201
|
+
'Entity evaluator requires a literal name.'
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
return entity(
|
|
206
|
+
name,
|
|
207
|
+
shape as z.ZodRawShape,
|
|
208
|
+
resolveEntityOptions(shape, options)
|
|
209
|
+
);
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
const evaluateMemberCall = (
|
|
213
|
+
callee: AstNode,
|
|
214
|
+
args: readonly unknown[],
|
|
215
|
+
env: EntityEvaluationEnvironment
|
|
216
|
+
): unknown => {
|
|
217
|
+
const receiver = evaluateNode(requireNode(getNodeObject(callee)), env);
|
|
218
|
+
const propertyName = getPropertyName(getNodeProperty(callee));
|
|
219
|
+
if (!propertyName) {
|
|
220
|
+
throw new UnsupportedEntityEvaluationError(
|
|
221
|
+
'Unsupported member property in entity evaluation.'
|
|
222
|
+
);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const method = (receiver as Record<string, unknown>)[propertyName];
|
|
226
|
+
if (typeof method !== 'function') {
|
|
227
|
+
throw new UnsupportedEntityEvaluationError(
|
|
228
|
+
`Entity evaluator could not call "${propertyName}".`
|
|
229
|
+
);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
return Reflect.apply(method, receiver, args);
|
|
233
|
+
};
|
|
234
|
+
|
|
235
|
+
const evaluateCallExpression: EntityNodeEvaluator = (
|
|
236
|
+
node: AstNode,
|
|
237
|
+
env: EntityEvaluationEnvironment
|
|
238
|
+
): unknown => {
|
|
239
|
+
const args = evaluateCallArguments(node, env);
|
|
240
|
+
const callee = requireNode(node['callee'] as AstNode | undefined);
|
|
241
|
+
|
|
242
|
+
if (callee.type === 'Identifier') {
|
|
243
|
+
const calleeName = identifierName(callee);
|
|
244
|
+
if (calleeName !== 'entity') {
|
|
245
|
+
throw new UnsupportedEntityEvaluationError(
|
|
246
|
+
`Unsupported entity evaluator call "${calleeName ?? '<unknown>'}".`
|
|
247
|
+
);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
return evaluateEntityCall(args);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
if (
|
|
254
|
+
callee.type !== 'MemberExpression' &&
|
|
255
|
+
callee.type !== 'StaticMemberExpression'
|
|
256
|
+
) {
|
|
257
|
+
throw new UnsupportedEntityEvaluationError(
|
|
258
|
+
`Unsupported callee type "${callee.type}".`
|
|
259
|
+
);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
return evaluateMemberCall(callee, args, env);
|
|
263
|
+
};
|
|
264
|
+
|
|
265
|
+
const EVALUATOR_REGISTRATIONS: readonly (readonly [
|
|
266
|
+
string,
|
|
267
|
+
EntityNodeEvaluator,
|
|
268
|
+
])[] = [
|
|
269
|
+
['ArrayExpression', evaluateArrayExpression],
|
|
270
|
+
['BooleanLiteral', evaluateLiteralExpression],
|
|
271
|
+
['Literal', evaluateLiteralExpression],
|
|
272
|
+
['NumericLiteral', evaluateLiteralExpression],
|
|
273
|
+
['StringLiteral', evaluateLiteralExpression],
|
|
274
|
+
['CallExpression', evaluateCallExpression],
|
|
275
|
+
['Identifier', evaluateIdentifierExpression],
|
|
276
|
+
['NullLiteral', evaluateNullExpression],
|
|
277
|
+
['ObjectExpression', evaluateObjectExpression],
|
|
278
|
+
['ParenthesizedExpression', evaluateWrappedExpression],
|
|
279
|
+
['TSAsExpression', evaluateWrappedExpression],
|
|
280
|
+
['TSSatisfiesExpression', evaluateWrappedExpression],
|
|
281
|
+
];
|
|
282
|
+
|
|
283
|
+
for (const [type, evaluator] of EVALUATOR_REGISTRATIONS) {
|
|
284
|
+
entityNodeEvaluators.set(type, evaluator);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
const hasExamples = (definition: EntityDefinition): boolean =>
|
|
288
|
+
definition.options !== null &&
|
|
289
|
+
findConfigProperty(definition.options, 'examples') !== null;
|
|
290
|
+
|
|
291
|
+
const evaluateEntityDefinition = (
|
|
292
|
+
definition: EntityDefinition,
|
|
293
|
+
env: EntityEvaluationEnvironment
|
|
294
|
+
): unknown => evaluateNode(definition.call, env);
|
|
295
|
+
|
|
296
|
+
const buildEntityExampleErrorDiagnostic = (
|
|
297
|
+
definition: EntityDefinition,
|
|
298
|
+
error: unknown,
|
|
299
|
+
sourceCode: string,
|
|
300
|
+
filePath: string
|
|
301
|
+
): WardenDiagnostic | null => {
|
|
302
|
+
if (
|
|
303
|
+
error instanceof UnsupportedEntityEvaluationError ||
|
|
304
|
+
!hasExamples(definition) ||
|
|
305
|
+
!(error instanceof Error)
|
|
306
|
+
) {
|
|
307
|
+
return null;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
return buildInvalidExampleDiagnostic(
|
|
311
|
+
definition.name,
|
|
312
|
+
error.message,
|
|
313
|
+
filePath,
|
|
314
|
+
offsetToLine(sourceCode, definition.start)
|
|
315
|
+
);
|
|
316
|
+
};
|
|
317
|
+
|
|
318
|
+
const evaluateEntityExamples = (
|
|
319
|
+
definition: EntityDefinition,
|
|
320
|
+
env: Map<string, unknown>,
|
|
321
|
+
sourceCode: string,
|
|
322
|
+
filePath: string
|
|
323
|
+
): WardenDiagnostic | null => {
|
|
324
|
+
try {
|
|
325
|
+
const value = evaluateEntityDefinition(definition, env);
|
|
326
|
+
if (definition.bindingName) {
|
|
327
|
+
env.set(definition.bindingName, value);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
return null;
|
|
331
|
+
} catch (error) {
|
|
332
|
+
return buildEntityExampleErrorDiagnostic(
|
|
333
|
+
definition,
|
|
334
|
+
error,
|
|
335
|
+
sourceCode,
|
|
336
|
+
filePath
|
|
337
|
+
);
|
|
338
|
+
}
|
|
339
|
+
};
|
|
340
|
+
|
|
341
|
+
const collectEntityExampleDiagnostics = (
|
|
342
|
+
definitions: readonly EntityDefinition[],
|
|
343
|
+
sourceCode: string,
|
|
344
|
+
filePath: string
|
|
345
|
+
): readonly WardenDiagnostic[] => {
|
|
346
|
+
const diagnostics: WardenDiagnostic[] = [];
|
|
347
|
+
const env = new Map<string, unknown>();
|
|
348
|
+
|
|
349
|
+
for (const definition of definitions) {
|
|
350
|
+
const diagnostic = evaluateEntityExamples(
|
|
351
|
+
definition,
|
|
352
|
+
env,
|
|
353
|
+
sourceCode,
|
|
354
|
+
filePath
|
|
355
|
+
);
|
|
356
|
+
if (diagnostic) {
|
|
357
|
+
diagnostics.push(diagnostic);
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
return diagnostics;
|
|
362
|
+
};
|
|
363
|
+
|
|
364
|
+
const checkEntityExamples = (
|
|
365
|
+
sourceCode: string,
|
|
366
|
+
filePath: string
|
|
367
|
+
): readonly WardenDiagnostic[] => {
|
|
368
|
+
if (isTestFile(filePath)) {
|
|
369
|
+
return [];
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
const ast = parse(filePath, sourceCode);
|
|
373
|
+
if (!ast) {
|
|
374
|
+
return [];
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
return collectEntityExampleDiagnostics(
|
|
378
|
+
findEntityDefinitions(ast),
|
|
379
|
+
sourceCode,
|
|
380
|
+
filePath
|
|
381
|
+
);
|
|
382
|
+
};
|
|
383
|
+
|
|
384
|
+
/**
|
|
385
|
+
* Checks that entity examples validate against their schema.
|
|
386
|
+
*/
|
|
387
|
+
export const exampleValid: WardenRule = {
|
|
388
|
+
check(sourceCode: string, filePath: string): readonly WardenDiagnostic[] {
|
|
389
|
+
return checkEntityExamples(sourceCode, filePath);
|
|
390
|
+
},
|
|
391
|
+
description:
|
|
392
|
+
'Ensure every entity example validates against the declared entity schema.',
|
|
393
|
+
name: 'example-valid',
|
|
394
|
+
severity: 'error',
|
|
395
|
+
};
|