@ontrails/warden 1.0.0-beta.2 → 1.0.0-beta.21
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 +497 -6
- package/README.md +77 -26
- package/bin/warden.ts +50 -0
- package/package.json +27 -5
- package/src/adapter-check.ts +136 -0
- package/src/ast.ts +28 -0
- package/src/cli.ts +1374 -103
- package/src/command.ts +953 -0
- package/src/config.ts +184 -0
- package/src/draft.ts +22 -0
- package/src/drift.ts +106 -22
- package/src/fix.ts +120 -0
- package/src/formatters.ts +79 -9
- package/src/guide.ts +245 -0
- package/src/index.ts +206 -14
- package/src/project-context.ts +163 -0
- package/src/resolve.ts +530 -0
- package/src/rules/activation-orphan.ts +97 -0
- package/src/rules/ast.ts +3176 -85
- package/src/rules/circular-refs.ts +154 -0
- package/src/rules/composes-declarations.ts +704 -0
- package/src/rules/context-no-surface-types.ts +68 -8
- package/src/rules/contour-exists.ts +251 -0
- package/src/rules/contour-ids.ts +15 -0
- package/src/rules/dead-internal-trail.ts +154 -0
- package/src/rules/draft-file-marking.ts +160 -0
- package/src/rules/draft-visible-debt.ts +87 -0
- package/src/rules/error-mapping-completeness.ts +288 -0
- package/src/rules/example-valid.ts +401 -0
- package/src/rules/fires-declarations.ts +758 -0
- package/src/rules/implementation-returns-result.ts +1265 -95
- package/src/rules/incomplete-accessor-for-standard-op.ts +272 -0
- package/src/rules/incomplete-crud.ts +580 -0
- package/src/rules/index.ts +219 -18
- package/src/rules/intent-propagation.ts +127 -0
- package/src/rules/layer-field-name-drift.ts +96 -0
- package/src/rules/metadata.ts +654 -0
- package/src/rules/missing-reconcile.ts +98 -0
- package/src/rules/missing-visibility.ts +110 -0
- package/src/rules/no-destructured-compose.ts +192 -0
- package/src/rules/no-dev-permit-in-source.ts +99 -0
- package/src/rules/no-direct-implementation-call.ts +7 -7
- package/src/rules/no-legacy-layer-imports.ts +211 -0
- package/src/rules/no-native-error-result.ts +111 -0
- package/src/rules/no-redundant-result-error-wrap.ts +331 -0
- package/src/rules/no-retired-cross-vocabulary.ts +194 -0
- package/src/rules/no-sync-result-assumption.ts +1134 -99
- package/src/rules/no-throw-in-detour-recover.ts +225 -0
- package/src/rules/no-throw-in-implementation.ts +10 -9
- package/src/rules/no-top-level-surface.ts +389 -0
- package/src/rules/on-references-exist.ts +194 -0
- package/src/rules/orphaned-signal.ts +150 -0
- package/src/rules/owner-projection-parity.ts +146 -0
- package/src/rules/permit-governance.ts +25 -0
- package/src/rules/public-export-example-coverage.ts +553 -0
- package/src/rules/public-internal-deep-imports.ts +517 -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 +187 -0
- package/src/rules/reference-exists.ts +98 -0
- package/src/rules/registry-names.ts +145 -0
- package/src/rules/resolved-import-boundary.ts +146 -0
- package/src/rules/resource-declarations.ts +704 -0
- package/src/rules/resource-exists.ts +179 -0
- package/src/rules/resource-id-grammar.ts +65 -0
- package/src/rules/resource-mock-coverage.ts +115 -0
- package/src/rules/scan.ts +38 -25
- package/src/rules/scheduled-destroy-intent.ts +44 -0
- package/src/rules/signal-graph-coaching.ts +191 -0
- package/src/rules/specs.ts +9 -5
- package/src/rules/static-resource-accessor-preference.ts +657 -0
- package/src/rules/surface-facet-coherence.ts +370 -0
- package/src/rules/trail-versioning-source.ts +1094 -0
- package/src/rules/trail-versioning-topo.ts +172 -0
- package/src/rules/types.ts +270 -6
- package/src/rules/unmaterialized-activation-source.ts +84 -0
- package/src/rules/unreachable-detour-shadowing.ts +344 -0
- package/src/rules/valid-describe-refs.ts +160 -32
- package/src/rules/valid-detour-contract.ts +78 -0
- package/src/rules/warden-export-symmetry.ts +533 -0
- package/src/rules/warden-rules-use-ast.ts +996 -0
- package/src/rules/webhook-route-collision.ts +243 -0
- package/src/trails/activation-orphan.trail.ts +84 -0
- package/src/trails/circular-refs.trail.ts +29 -0
- package/src/trails/composes-declarations.trail.ts +22 -0
- package/src/trails/context-no-surface-types.trail.ts +21 -0
- package/src/trails/contour-exists.trail.ts +21 -0
- package/src/trails/dead-internal-trail.trail.ts +26 -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/error-mapping-completeness.trail.ts +29 -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-blaze.trail.ts +31 -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 +78 -0
- package/src/trails/intent-propagation.trail.ts +30 -0
- package/src/trails/layer-field-name-drift.trail.ts +39 -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-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 +162 -0
- package/src/trails/scheduled-destroy-intent.trail.ts +56 -0
- package/src/trails/schema.ts +194 -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-facet-coherence.trail.ts +25 -0
- package/src/trails/topo.ts +6 -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 +45 -0
- package/src/trails/webhook-route-collision.trail.ts +50 -0
- package/src/trails/wrap-rule.ts +213 -0
- package/src/workspaces.ts +238 -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 -221
- 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 -163
- 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 -231
- package/dist/rules/implementation-returns-result.js.map +0 -1
- package/dist/rules/index.d.ts +0 -22
- package/dist/rules/index.d.ts.map +0 -1
- package/dist/rules/index.js +0 -41
- 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 -46
- 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 -75
- 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 -188
- package/src/__tests__/valid-describe-refs.test.ts +0 -60
- package/src/rules/no-direct-impl-in-route.ts +0 -77
- 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,1094 @@
|
|
|
1
|
+
import {
|
|
2
|
+
findBlazeBodies,
|
|
3
|
+
findConfigProperty,
|
|
4
|
+
findTrailDefinitions,
|
|
5
|
+
isMemberAccessNonComputed,
|
|
6
|
+
offsetToLine,
|
|
7
|
+
parse,
|
|
8
|
+
walk,
|
|
9
|
+
walkScope,
|
|
10
|
+
} from './ast.js';
|
|
11
|
+
import type { AstNode } from './ast.js';
|
|
12
|
+
import type { WardenDiagnostic, WardenRule } from './types.js';
|
|
13
|
+
|
|
14
|
+
const VERSION_PINNED_COMPOSE = 'version-pinned-compose';
|
|
15
|
+
const FORK_WITHOUT_PRESERVED_BLAZE = 'fork-without-preserved-blaze';
|
|
16
|
+
const MARKER_SCHEMA_UNSUPPORTED = 'marker-schema-unsupported';
|
|
17
|
+
|
|
18
|
+
interface SchemaBindingRecord {
|
|
19
|
+
readonly initializer: AstNode | undefined;
|
|
20
|
+
readonly scopeEnd: number;
|
|
21
|
+
readonly scopeStart: number;
|
|
22
|
+
readonly start: number;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
type SchemaBindings = ReadonlyMap<string, readonly SchemaBindingRecord[]>;
|
|
26
|
+
|
|
27
|
+
// Zod schema constructors and modifiers outside the marker subset that the
|
|
28
|
+
// runtime guard in packages/core/src/version-marker.ts rejects. This deny-list
|
|
29
|
+
// is best-effort source-static coverage; the runtime allow-list remains the
|
|
30
|
+
// authoritative gate. Entries are evidence-verified against the runtime guard.
|
|
31
|
+
const unsupportedSchemaCalls = new Set([
|
|
32
|
+
'and',
|
|
33
|
+
'any',
|
|
34
|
+
'base64',
|
|
35
|
+
'base64url',
|
|
36
|
+
'bigint',
|
|
37
|
+
'catch',
|
|
38
|
+
'catchall',
|
|
39
|
+
'check',
|
|
40
|
+
'cidrv4',
|
|
41
|
+
'cidrv6',
|
|
42
|
+
'codec',
|
|
43
|
+
'cuid',
|
|
44
|
+
'cuid2',
|
|
45
|
+
'custom',
|
|
46
|
+
'date',
|
|
47
|
+
'datetime',
|
|
48
|
+
'default',
|
|
49
|
+
'duration',
|
|
50
|
+
'e164',
|
|
51
|
+
'email',
|
|
52
|
+
'emoji',
|
|
53
|
+
'endsWith',
|
|
54
|
+
'file',
|
|
55
|
+
'finite',
|
|
56
|
+
'function',
|
|
57
|
+
'gt',
|
|
58
|
+
'gte',
|
|
59
|
+
'guid',
|
|
60
|
+
'hash',
|
|
61
|
+
'includes',
|
|
62
|
+
'instanceof',
|
|
63
|
+
'int',
|
|
64
|
+
'intersection',
|
|
65
|
+
'ipv4',
|
|
66
|
+
'ipv6',
|
|
67
|
+
'json',
|
|
68
|
+
'jwt',
|
|
69
|
+
'ksuid',
|
|
70
|
+
'lazy',
|
|
71
|
+
'length',
|
|
72
|
+
'loose',
|
|
73
|
+
'looseObject',
|
|
74
|
+
'looseRecord',
|
|
75
|
+
'lowercase',
|
|
76
|
+
'lt',
|
|
77
|
+
'lte',
|
|
78
|
+
'map',
|
|
79
|
+
'max',
|
|
80
|
+
'min',
|
|
81
|
+
'multipleOf',
|
|
82
|
+
'nan',
|
|
83
|
+
'nanoid',
|
|
84
|
+
'negative',
|
|
85
|
+
'never',
|
|
86
|
+
'nonempty',
|
|
87
|
+
'nonnegative',
|
|
88
|
+
'nonoptional',
|
|
89
|
+
'nonpositive',
|
|
90
|
+
'normalize',
|
|
91
|
+
'null',
|
|
92
|
+
'overwrite',
|
|
93
|
+
'partialRecord',
|
|
94
|
+
'passthrough',
|
|
95
|
+
'pipe',
|
|
96
|
+
'positive',
|
|
97
|
+
'prefault',
|
|
98
|
+
'preprocess',
|
|
99
|
+
'promise',
|
|
100
|
+
'record',
|
|
101
|
+
'refine',
|
|
102
|
+
'regex',
|
|
103
|
+
'required',
|
|
104
|
+
'safe',
|
|
105
|
+
'set',
|
|
106
|
+
'slugify',
|
|
107
|
+
'startsWith',
|
|
108
|
+
'step',
|
|
109
|
+
'strict',
|
|
110
|
+
'strictObject',
|
|
111
|
+
'stringbool',
|
|
112
|
+
'superRefine',
|
|
113
|
+
'symbol',
|
|
114
|
+
'templateLiteral',
|
|
115
|
+
'time',
|
|
116
|
+
'toLowerCase',
|
|
117
|
+
'toUpperCase',
|
|
118
|
+
'transform',
|
|
119
|
+
'trim',
|
|
120
|
+
'tuple',
|
|
121
|
+
'ulid',
|
|
122
|
+
'undefined',
|
|
123
|
+
'unknown',
|
|
124
|
+
'uppercase',
|
|
125
|
+
'url',
|
|
126
|
+
'uuid',
|
|
127
|
+
'uuidv4',
|
|
128
|
+
'uuidv6',
|
|
129
|
+
'uuidv7',
|
|
130
|
+
'void',
|
|
131
|
+
'xid',
|
|
132
|
+
]);
|
|
133
|
+
|
|
134
|
+
const diagnostic = (
|
|
135
|
+
rule: string,
|
|
136
|
+
severity: WardenDiagnostic['severity'],
|
|
137
|
+
filePath: string,
|
|
138
|
+
sourceCode: string,
|
|
139
|
+
node: AstNode,
|
|
140
|
+
message: string
|
|
141
|
+
): WardenDiagnostic => ({
|
|
142
|
+
filePath,
|
|
143
|
+
line: offsetToLine(sourceCode, node.start),
|
|
144
|
+
message,
|
|
145
|
+
rule,
|
|
146
|
+
severity,
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
const staticPropertyKeyName = (node: AstNode | undefined): string | null => {
|
|
150
|
+
if (node?.type === 'Identifier') {
|
|
151
|
+
return (node as unknown as { name?: string }).name ?? null;
|
|
152
|
+
}
|
|
153
|
+
if (
|
|
154
|
+
node?.type === 'Literal' ||
|
|
155
|
+
node?.type === 'StringLiteral' ||
|
|
156
|
+
node?.type === 'NumericLiteral'
|
|
157
|
+
) {
|
|
158
|
+
const { value } = node as unknown as { value?: unknown };
|
|
159
|
+
return typeof value === 'string' || typeof value === 'number'
|
|
160
|
+
? String(value)
|
|
161
|
+
: null;
|
|
162
|
+
}
|
|
163
|
+
return null;
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
const objectProperties = (node: AstNode | undefined): readonly AstNode[] =>
|
|
167
|
+
node?.type === 'ObjectExpression'
|
|
168
|
+
? ((node as unknown as { properties?: readonly AstNode[] }).properties ??
|
|
169
|
+
[])
|
|
170
|
+
: [];
|
|
171
|
+
|
|
172
|
+
const propertyName = (node: AstNode): string | null =>
|
|
173
|
+
node.type === 'Property'
|
|
174
|
+
? staticPropertyKeyName((node as unknown as { key?: AstNode }).key)
|
|
175
|
+
: null;
|
|
176
|
+
|
|
177
|
+
const hasProperty = (node: AstNode, name: string): boolean =>
|
|
178
|
+
objectProperties(node).some((property) => propertyName(property) === name);
|
|
179
|
+
|
|
180
|
+
const propertyValue = (property: AstNode | null): AstNode | undefined =>
|
|
181
|
+
property?.type === 'Property'
|
|
182
|
+
? ((property as unknown as { value?: AstNode }).value ?? undefined)
|
|
183
|
+
: undefined;
|
|
184
|
+
|
|
185
|
+
const trailIsVersioned = (config: AstNode): boolean =>
|
|
186
|
+
findConfigProperty(config, 'version') !== null ||
|
|
187
|
+
findConfigProperty(config, 'versions') !== null;
|
|
188
|
+
|
|
189
|
+
const versionEntries = (config: AstNode): readonly AstNode[] => {
|
|
190
|
+
const versions = propertyValue(findConfigProperty(config, 'versions'));
|
|
191
|
+
return objectProperties(versions)
|
|
192
|
+
.map((property) => propertyValue(property))
|
|
193
|
+
.filter((entry): entry is AstNode => entry?.type === 'ObjectExpression');
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
const identifierName = (node: AstNode | undefined): string | undefined =>
|
|
197
|
+
node?.type === 'Identifier'
|
|
198
|
+
? (node as unknown as { name?: string }).name
|
|
199
|
+
: undefined;
|
|
200
|
+
|
|
201
|
+
const schemaBindingInitializer = (
|
|
202
|
+
schemaBindings: SchemaBindings,
|
|
203
|
+
name: string,
|
|
204
|
+
referenceStart: number
|
|
205
|
+
): AstNode | undefined => {
|
|
206
|
+
let resolved: SchemaBindingRecord | undefined;
|
|
207
|
+
for (const record of schemaBindings.get(name) ?? []) {
|
|
208
|
+
if (record.start >= referenceStart) {
|
|
209
|
+
break;
|
|
210
|
+
}
|
|
211
|
+
if (
|
|
212
|
+
record.scopeStart > referenceStart ||
|
|
213
|
+
record.scopeEnd < referenceStart
|
|
214
|
+
) {
|
|
215
|
+
continue;
|
|
216
|
+
}
|
|
217
|
+
resolved = record;
|
|
218
|
+
}
|
|
219
|
+
return resolved?.initializer;
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
const memberObject = (node: AstNode | undefined): AstNode | undefined =>
|
|
223
|
+
node !== undefined && isMemberAccessNonComputed(node)
|
|
224
|
+
? (node as unknown as { object?: AstNode }).object
|
|
225
|
+
: undefined;
|
|
226
|
+
|
|
227
|
+
const memberPropertyName = (node: AstNode | undefined): string | undefined =>
|
|
228
|
+
node !== undefined && isMemberAccessNonComputed(node)
|
|
229
|
+
? identifierName((node as unknown as { property?: AstNode }).property)
|
|
230
|
+
: undefined;
|
|
231
|
+
|
|
232
|
+
const callCallee = (node: AstNode): AstNode | undefined =>
|
|
233
|
+
node.type === 'CallExpression'
|
|
234
|
+
? (node as unknown as { callee?: AstNode }).callee
|
|
235
|
+
: undefined;
|
|
236
|
+
|
|
237
|
+
const isZodSchemaReceiver = (
|
|
238
|
+
node: AstNode | undefined,
|
|
239
|
+
schemaBindings: SchemaBindings = new Map(),
|
|
240
|
+
referenceStart = node?.start ?? Number.POSITIVE_INFINITY
|
|
241
|
+
): boolean => {
|
|
242
|
+
if (!node) {
|
|
243
|
+
return false;
|
|
244
|
+
}
|
|
245
|
+
const name = identifierName(node);
|
|
246
|
+
if (
|
|
247
|
+
name === 'z' ||
|
|
248
|
+
(name !== undefined &&
|
|
249
|
+
schemaBindingInitializer(schemaBindings, name, referenceStart) !==
|
|
250
|
+
undefined)
|
|
251
|
+
) {
|
|
252
|
+
return true;
|
|
253
|
+
}
|
|
254
|
+
if (node.type === 'CallExpression') {
|
|
255
|
+
return isZodSchemaReceiver(
|
|
256
|
+
memberObject(callCallee(node)),
|
|
257
|
+
schemaBindings,
|
|
258
|
+
referenceStart
|
|
259
|
+
);
|
|
260
|
+
}
|
|
261
|
+
return isZodSchemaReceiver(
|
|
262
|
+
memberObject(node),
|
|
263
|
+
schemaBindings,
|
|
264
|
+
referenceStart
|
|
265
|
+
);
|
|
266
|
+
};
|
|
267
|
+
|
|
268
|
+
const isZodSchemaCallee = (
|
|
269
|
+
node: AstNode | undefined,
|
|
270
|
+
schemaBindings: SchemaBindings = new Map()
|
|
271
|
+
): boolean =>
|
|
272
|
+
node !== undefined && isZodSchemaReceiver(memberObject(node), schemaBindings);
|
|
273
|
+
|
|
274
|
+
const callArguments = (node: AstNode): readonly AstNode[] =>
|
|
275
|
+
node.type === 'CallExpression'
|
|
276
|
+
? ((node as unknown as { arguments?: readonly AstNode[] }).arguments ?? [])
|
|
277
|
+
: [];
|
|
278
|
+
|
|
279
|
+
const unwrapExpression = (node: AstNode | undefined): AstNode | undefined => {
|
|
280
|
+
let current = node;
|
|
281
|
+
while (
|
|
282
|
+
current?.type === 'TSAsExpression' ||
|
|
283
|
+
current?.type === 'TSSatisfiesExpression' ||
|
|
284
|
+
current?.type === 'TSNonNullExpression'
|
|
285
|
+
) {
|
|
286
|
+
current = (current as unknown as { expression?: AstNode }).expression;
|
|
287
|
+
}
|
|
288
|
+
return current;
|
|
289
|
+
};
|
|
290
|
+
|
|
291
|
+
const arrayExpressionLength = (node: AstNode | undefined): number => {
|
|
292
|
+
const expression = unwrapExpression(node);
|
|
293
|
+
return expression?.type === 'ArrayExpression'
|
|
294
|
+
? (
|
|
295
|
+
(expression as unknown as { elements?: readonly unknown[] }).elements ??
|
|
296
|
+
[]
|
|
297
|
+
).length
|
|
298
|
+
: 0;
|
|
299
|
+
};
|
|
300
|
+
|
|
301
|
+
const isNumberProperty = (
|
|
302
|
+
node: AstNode | undefined,
|
|
303
|
+
names: ReadonlySet<string>
|
|
304
|
+
): boolean =>
|
|
305
|
+
node !== undefined &&
|
|
306
|
+
isMemberAccessNonComputed(node) &&
|
|
307
|
+
identifierName(memberObject(node)) === 'Number' &&
|
|
308
|
+
names.has(memberPropertyName(node) ?? '');
|
|
309
|
+
|
|
310
|
+
const nonFiniteNumberProperties = new Set([
|
|
311
|
+
'NaN',
|
|
312
|
+
'NEGATIVE_INFINITY',
|
|
313
|
+
'POSITIVE_INFINITY',
|
|
314
|
+
]);
|
|
315
|
+
|
|
316
|
+
const literalExpressionIsJsonLossy = (expression: AstNode): boolean => {
|
|
317
|
+
if (
|
|
318
|
+
expression.type === 'BigIntLiteral' ||
|
|
319
|
+
expression.type === 'RegExpLiteral'
|
|
320
|
+
) {
|
|
321
|
+
return true;
|
|
322
|
+
}
|
|
323
|
+
const literal = expression as unknown as {
|
|
324
|
+
readonly bigint?: unknown;
|
|
325
|
+
readonly regex?: unknown;
|
|
326
|
+
readonly value?: unknown;
|
|
327
|
+
};
|
|
328
|
+
if (literal.bigint !== undefined || literal.regex !== undefined) {
|
|
329
|
+
return true;
|
|
330
|
+
}
|
|
331
|
+
if (literal.value instanceof RegExp) {
|
|
332
|
+
return true;
|
|
333
|
+
}
|
|
334
|
+
return typeof literal.value === 'number' && !Number.isFinite(literal.value);
|
|
335
|
+
};
|
|
336
|
+
|
|
337
|
+
const expressionIsJsonLossy = (node: AstNode | undefined): boolean => {
|
|
338
|
+
const expression = unwrapExpression(node);
|
|
339
|
+
if (!expression) {
|
|
340
|
+
return true;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
const name = identifierName(expression);
|
|
344
|
+
if (name === 'NaN' || name === 'Infinity' || name === 'undefined') {
|
|
345
|
+
return true;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
if (isNumberProperty(expression, nonFiniteNumberProperties)) {
|
|
349
|
+
return true;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
if (expression.type === 'UnaryExpression') {
|
|
353
|
+
return expressionIsJsonLossy(
|
|
354
|
+
(expression as unknown as { argument?: AstNode }).argument
|
|
355
|
+
);
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
if (expression.type === 'Literal' || expression.type === 'NumericLiteral') {
|
|
359
|
+
return literalExpressionIsJsonLossy(expression);
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
if (expression.type === 'ArrayExpression') {
|
|
363
|
+
const elements =
|
|
364
|
+
(expression as unknown as { elements?: readonly (AstNode | null)[] })
|
|
365
|
+
.elements ?? [];
|
|
366
|
+
return elements.some((element) =>
|
|
367
|
+
expressionIsJsonLossy(element ?? undefined)
|
|
368
|
+
);
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
if (expression.type === 'ObjectExpression') {
|
|
372
|
+
return objectProperties(expression).some((property) => {
|
|
373
|
+
if (property.type !== 'Property') {
|
|
374
|
+
return false;
|
|
375
|
+
}
|
|
376
|
+
return expressionIsJsonLossy(propertyValue(property));
|
|
377
|
+
});
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
return false;
|
|
381
|
+
};
|
|
382
|
+
|
|
383
|
+
const expressionIsReferenceValued = (node: AstNode | undefined): boolean => {
|
|
384
|
+
const expression = unwrapExpression(node);
|
|
385
|
+
return (
|
|
386
|
+
expression?.type === 'ArrayExpression' ||
|
|
387
|
+
expression?.type === 'ObjectExpression'
|
|
388
|
+
);
|
|
389
|
+
};
|
|
390
|
+
|
|
391
|
+
const isMultiValueLiteralCall = (
|
|
392
|
+
node: AstNode,
|
|
393
|
+
schemaBindings: SchemaBindings
|
|
394
|
+
): boolean => {
|
|
395
|
+
const callee = callCallee(node);
|
|
396
|
+
return (
|
|
397
|
+
node.type === 'CallExpression' &&
|
|
398
|
+
memberPropertyName(callee) === 'literal' &&
|
|
399
|
+
isZodSchemaCallee(callee, schemaBindings) &&
|
|
400
|
+
arrayExpressionLength(callArguments(node)[0]) > 1
|
|
401
|
+
);
|
|
402
|
+
};
|
|
403
|
+
|
|
404
|
+
const isJsonLossyLiteralCall = (
|
|
405
|
+
node: AstNode,
|
|
406
|
+
schemaBindings: SchemaBindings
|
|
407
|
+
): boolean => {
|
|
408
|
+
const callee = callCallee(node);
|
|
409
|
+
return (
|
|
410
|
+
node.type === 'CallExpression' &&
|
|
411
|
+
memberPropertyName(callee) === 'literal' &&
|
|
412
|
+
isZodSchemaCallee(callee, schemaBindings) &&
|
|
413
|
+
expressionIsJsonLossy(callArguments(node)[0])
|
|
414
|
+
);
|
|
415
|
+
};
|
|
416
|
+
|
|
417
|
+
const isReferenceValuedLiteralCall = (
|
|
418
|
+
node: AstNode,
|
|
419
|
+
schemaBindings: SchemaBindings
|
|
420
|
+
): boolean => {
|
|
421
|
+
const callee = callCallee(node);
|
|
422
|
+
if (
|
|
423
|
+
node.type !== 'CallExpression' ||
|
|
424
|
+
memberPropertyName(callee) !== 'literal' ||
|
|
425
|
+
!isZodSchemaCallee(callee, schemaBindings)
|
|
426
|
+
) {
|
|
427
|
+
return false;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
const [rawValue] = callArguments(node);
|
|
431
|
+
const value = unwrapExpression(rawValue);
|
|
432
|
+
if (value?.type === 'ObjectExpression') {
|
|
433
|
+
return true;
|
|
434
|
+
}
|
|
435
|
+
if (value?.type !== 'ArrayExpression') {
|
|
436
|
+
return false;
|
|
437
|
+
}
|
|
438
|
+
return (
|
|
439
|
+
(value as unknown as { elements?: readonly (AstNode | null)[] }).elements ??
|
|
440
|
+
[]
|
|
441
|
+
).some((element) => expressionIsReferenceValued(element ?? undefined));
|
|
442
|
+
};
|
|
443
|
+
|
|
444
|
+
const isJsonLossyEnumCall = (
|
|
445
|
+
node: AstNode,
|
|
446
|
+
schemaBindings: SchemaBindings
|
|
447
|
+
): boolean => {
|
|
448
|
+
const callee = callCallee(node);
|
|
449
|
+
if (
|
|
450
|
+
node.type !== 'CallExpression' ||
|
|
451
|
+
memberPropertyName(callee) !== 'enum' ||
|
|
452
|
+
!isZodSchemaCallee(callee, schemaBindings)
|
|
453
|
+
) {
|
|
454
|
+
return false;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
const [rawOptions] = callArguments(node);
|
|
458
|
+
const options = unwrapExpression(rawOptions);
|
|
459
|
+
if (options?.type === 'ArrayExpression') {
|
|
460
|
+
return expressionIsJsonLossy(options);
|
|
461
|
+
}
|
|
462
|
+
if (options?.type !== 'ObjectExpression') {
|
|
463
|
+
return false;
|
|
464
|
+
}
|
|
465
|
+
return objectProperties(options).some((property) => {
|
|
466
|
+
if (property.type !== 'Property') {
|
|
467
|
+
return false;
|
|
468
|
+
}
|
|
469
|
+
return expressionIsJsonLossy(propertyValue(property));
|
|
470
|
+
});
|
|
471
|
+
};
|
|
472
|
+
|
|
473
|
+
const isReferenceValuedEnumCall = (
|
|
474
|
+
node: AstNode,
|
|
475
|
+
schemaBindings: SchemaBindings
|
|
476
|
+
): boolean => {
|
|
477
|
+
const callee = callCallee(node);
|
|
478
|
+
if (
|
|
479
|
+
node.type !== 'CallExpression' ||
|
|
480
|
+
memberPropertyName(callee) !== 'enum' ||
|
|
481
|
+
!isZodSchemaCallee(callee, schemaBindings)
|
|
482
|
+
) {
|
|
483
|
+
return false;
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
const [rawOptions] = callArguments(node);
|
|
487
|
+
const options = unwrapExpression(rawOptions);
|
|
488
|
+
if (options?.type === 'ArrayExpression') {
|
|
489
|
+
return (
|
|
490
|
+
(options as unknown as { elements?: readonly (AstNode | null)[] })
|
|
491
|
+
.elements ?? []
|
|
492
|
+
).some((element) => expressionIsReferenceValued(element ?? undefined));
|
|
493
|
+
}
|
|
494
|
+
if (options?.type !== 'ObjectExpression') {
|
|
495
|
+
return false;
|
|
496
|
+
}
|
|
497
|
+
return objectProperties(options).some((property) => {
|
|
498
|
+
if (property.type !== 'Property') {
|
|
499
|
+
return false;
|
|
500
|
+
}
|
|
501
|
+
return expressionIsReferenceValued(propertyValue(property));
|
|
502
|
+
});
|
|
503
|
+
};
|
|
504
|
+
|
|
505
|
+
const markerWrapperCanHideOptional = new Set(['nullable', 'readonly']);
|
|
506
|
+
|
|
507
|
+
const isOptionalWrapperCall = (
|
|
508
|
+
node: AstNode,
|
|
509
|
+
schemaBindings: SchemaBindings
|
|
510
|
+
): boolean => {
|
|
511
|
+
const callee = callCallee(node);
|
|
512
|
+
return (
|
|
513
|
+
node.type === 'CallExpression' &&
|
|
514
|
+
memberPropertyName(callee) === 'optional' &&
|
|
515
|
+
isZodSchemaCallee(callee, schemaBindings)
|
|
516
|
+
);
|
|
517
|
+
};
|
|
518
|
+
|
|
519
|
+
const callChainHasOptionalWrapper = (
|
|
520
|
+
node: AstNode | undefined,
|
|
521
|
+
schemaBindings: SchemaBindings
|
|
522
|
+
): boolean => {
|
|
523
|
+
const seen = new Set<number>();
|
|
524
|
+
const visit = (current: AstNode | undefined): boolean => {
|
|
525
|
+
const expression = unwrapExpression(current);
|
|
526
|
+
if (expression === undefined || seen.has(expression.start)) {
|
|
527
|
+
return false;
|
|
528
|
+
}
|
|
529
|
+
seen.add(expression.start);
|
|
530
|
+
|
|
531
|
+
const name = identifierName(expression);
|
|
532
|
+
if (name !== undefined) {
|
|
533
|
+
return visit(
|
|
534
|
+
schemaBindingInitializer(schemaBindings, name, expression.start)
|
|
535
|
+
);
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
if (expression.type === 'CallExpression') {
|
|
539
|
+
const callee = callCallee(expression);
|
|
540
|
+
if (
|
|
541
|
+
memberPropertyName(callee) === 'optional' &&
|
|
542
|
+
isZodSchemaCallee(callee, schemaBindings)
|
|
543
|
+
) {
|
|
544
|
+
return true;
|
|
545
|
+
}
|
|
546
|
+
return visit(memberObject(callee));
|
|
547
|
+
}
|
|
548
|
+
if (!isMemberAccessNonComputed(expression)) {
|
|
549
|
+
return false;
|
|
550
|
+
}
|
|
551
|
+
return visit(memberObject(expression));
|
|
552
|
+
};
|
|
553
|
+
return visit(node);
|
|
554
|
+
};
|
|
555
|
+
|
|
556
|
+
const isHiddenOptionalWrapperCall = (
|
|
557
|
+
node: AstNode,
|
|
558
|
+
schemaBindings: SchemaBindings
|
|
559
|
+
): boolean => {
|
|
560
|
+
const callee = callCallee(node);
|
|
561
|
+
return (
|
|
562
|
+
node.type === 'CallExpression' &&
|
|
563
|
+
markerWrapperCanHideOptional.has(memberPropertyName(callee) ?? '') &&
|
|
564
|
+
isZodSchemaCallee(callee, schemaBindings) &&
|
|
565
|
+
callChainHasOptionalWrapper(memberObject(callee), schemaBindings)
|
|
566
|
+
);
|
|
567
|
+
};
|
|
568
|
+
|
|
569
|
+
const nestedSchemaArguments = (node: AstNode): readonly AstNode[] => {
|
|
570
|
+
if (node.type !== 'CallExpression') {
|
|
571
|
+
return [];
|
|
572
|
+
}
|
|
573
|
+
const name = memberPropertyName(callCallee(node));
|
|
574
|
+
if (name === 'array') {
|
|
575
|
+
return callArguments(node).slice(0, 1);
|
|
576
|
+
}
|
|
577
|
+
if (name === 'or') {
|
|
578
|
+
return callArguments(node).slice(0, 1);
|
|
579
|
+
}
|
|
580
|
+
if (name !== 'union') {
|
|
581
|
+
return [];
|
|
582
|
+
}
|
|
583
|
+
const [rawOptions] = callArguments(node);
|
|
584
|
+
const options = unwrapExpression(rawOptions);
|
|
585
|
+
return options?.type === 'ArrayExpression'
|
|
586
|
+
? (
|
|
587
|
+
(options as unknown as { elements?: readonly (AstNode | null)[] })
|
|
588
|
+
.elements ?? []
|
|
589
|
+
).filter((element): element is AstNode => element !== null)
|
|
590
|
+
: [];
|
|
591
|
+
};
|
|
592
|
+
|
|
593
|
+
const collectUnsupportedOptionalWrapperStarts = (
|
|
594
|
+
node: AstNode,
|
|
595
|
+
schemaBindings: SchemaBindings,
|
|
596
|
+
unsupported: Set<number>,
|
|
597
|
+
options: { readonly optionalWrapperAllowed?: boolean } = {}
|
|
598
|
+
): void => {
|
|
599
|
+
const expression = unwrapExpression(node);
|
|
600
|
+
if (expression === undefined) {
|
|
601
|
+
return;
|
|
602
|
+
}
|
|
603
|
+
const name = identifierName(expression);
|
|
604
|
+
if (name !== undefined) {
|
|
605
|
+
const initializer = schemaBindingInitializer(
|
|
606
|
+
schemaBindings,
|
|
607
|
+
name,
|
|
608
|
+
expression.start
|
|
609
|
+
);
|
|
610
|
+
if (initializer !== undefined) {
|
|
611
|
+
collectUnsupportedOptionalWrapperStarts(
|
|
612
|
+
initializer,
|
|
613
|
+
schemaBindings,
|
|
614
|
+
unsupported,
|
|
615
|
+
options
|
|
616
|
+
);
|
|
617
|
+
}
|
|
618
|
+
return;
|
|
619
|
+
}
|
|
620
|
+
if (isOptionalWrapperCall(expression, schemaBindings)) {
|
|
621
|
+
if (options.optionalWrapperAllowed !== true) {
|
|
622
|
+
unsupported.add(expression.start);
|
|
623
|
+
}
|
|
624
|
+
const inner = memberObject(callCallee(expression));
|
|
625
|
+
if (inner) {
|
|
626
|
+
collectUnsupportedOptionalWrapperStarts(
|
|
627
|
+
inner,
|
|
628
|
+
schemaBindings,
|
|
629
|
+
unsupported
|
|
630
|
+
);
|
|
631
|
+
}
|
|
632
|
+
return;
|
|
633
|
+
}
|
|
634
|
+
if (expression.type === 'ObjectExpression') {
|
|
635
|
+
for (const property of objectProperties(expression)) {
|
|
636
|
+
collectUnsupportedOptionalWrapperStarts(
|
|
637
|
+
propertyValue(property) ?? property,
|
|
638
|
+
schemaBindings,
|
|
639
|
+
unsupported,
|
|
640
|
+
{ optionalWrapperAllowed: true }
|
|
641
|
+
);
|
|
642
|
+
}
|
|
643
|
+
return;
|
|
644
|
+
}
|
|
645
|
+
if (
|
|
646
|
+
expression.type === 'CallExpression' &&
|
|
647
|
+
memberPropertyName(callCallee(expression)) === 'object'
|
|
648
|
+
) {
|
|
649
|
+
const [rawShape] = callArguments(expression);
|
|
650
|
+
const shape = unwrapExpression(rawShape);
|
|
651
|
+
if (shape?.type === 'ObjectExpression') {
|
|
652
|
+
for (const property of objectProperties(shape)) {
|
|
653
|
+
collectUnsupportedOptionalWrapperStarts(
|
|
654
|
+
propertyValue(property) ?? property,
|
|
655
|
+
schemaBindings,
|
|
656
|
+
unsupported,
|
|
657
|
+
{ optionalWrapperAllowed: true }
|
|
658
|
+
);
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
return;
|
|
662
|
+
}
|
|
663
|
+
for (const argument of nestedSchemaArguments(expression)) {
|
|
664
|
+
collectUnsupportedOptionalWrapperStarts(
|
|
665
|
+
argument,
|
|
666
|
+
schemaBindings,
|
|
667
|
+
unsupported
|
|
668
|
+
);
|
|
669
|
+
}
|
|
670
|
+
};
|
|
671
|
+
|
|
672
|
+
const isMemberCallNamed = (
|
|
673
|
+
node: AstNode,
|
|
674
|
+
names: ReadonlySet<string>,
|
|
675
|
+
schemaBindings: SchemaBindings
|
|
676
|
+
): boolean => {
|
|
677
|
+
if (node.type !== 'CallExpression') {
|
|
678
|
+
return false;
|
|
679
|
+
}
|
|
680
|
+
const callee = callCallee(node);
|
|
681
|
+
if (!callee) {
|
|
682
|
+
return false;
|
|
683
|
+
}
|
|
684
|
+
if (!isZodSchemaCallee(callee, schemaBindings)) {
|
|
685
|
+
return false;
|
|
686
|
+
}
|
|
687
|
+
return names.has(memberPropertyName(callee) ?? '');
|
|
688
|
+
};
|
|
689
|
+
|
|
690
|
+
const bindingName = (node: AstNode): string | undefined =>
|
|
691
|
+
node.type === 'VariableDeclarator'
|
|
692
|
+
? identifierName((node as unknown as { id?: AstNode }).id)
|
|
693
|
+
: undefined;
|
|
694
|
+
|
|
695
|
+
const lexicalScopeTypes = new Set([
|
|
696
|
+
'ArrowFunctionExpression',
|
|
697
|
+
'BlockStatement',
|
|
698
|
+
'FunctionDeclaration',
|
|
699
|
+
'FunctionExpression',
|
|
700
|
+
'Program',
|
|
701
|
+
'StaticBlock',
|
|
702
|
+
]);
|
|
703
|
+
|
|
704
|
+
const addPatternBindingNames = (
|
|
705
|
+
node: AstNode | undefined,
|
|
706
|
+
into: Set<string>
|
|
707
|
+
) => {
|
|
708
|
+
if (!node) {
|
|
709
|
+
return;
|
|
710
|
+
}
|
|
711
|
+
if (node.type === 'Identifier') {
|
|
712
|
+
const name = identifierName(node);
|
|
713
|
+
if (name !== undefined) {
|
|
714
|
+
into.add(name);
|
|
715
|
+
}
|
|
716
|
+
return;
|
|
717
|
+
}
|
|
718
|
+
if (node.type === 'AssignmentPattern') {
|
|
719
|
+
addPatternBindingNames((node as unknown as { left?: AstNode }).left, into);
|
|
720
|
+
return;
|
|
721
|
+
}
|
|
722
|
+
if (node.type === 'RestElement') {
|
|
723
|
+
addPatternBindingNames(
|
|
724
|
+
(node as unknown as { argument?: AstNode }).argument,
|
|
725
|
+
into
|
|
726
|
+
);
|
|
727
|
+
return;
|
|
728
|
+
}
|
|
729
|
+
if (node.type === 'ArrayPattern') {
|
|
730
|
+
const elements =
|
|
731
|
+
(node as unknown as { elements?: readonly (AstNode | null)[] })
|
|
732
|
+
.elements ?? [];
|
|
733
|
+
for (const element of elements) {
|
|
734
|
+
addPatternBindingNames(element ?? undefined, into);
|
|
735
|
+
}
|
|
736
|
+
return;
|
|
737
|
+
}
|
|
738
|
+
if (node.type !== 'ObjectPattern') {
|
|
739
|
+
return;
|
|
740
|
+
}
|
|
741
|
+
const properties =
|
|
742
|
+
(node as unknown as { properties?: readonly AstNode[] }).properties ?? [];
|
|
743
|
+
for (const property of properties) {
|
|
744
|
+
if (property.type === 'RestElement') {
|
|
745
|
+
addPatternBindingNames(property, into);
|
|
746
|
+
continue;
|
|
747
|
+
}
|
|
748
|
+
addPatternBindingNames(
|
|
749
|
+
(property as unknown as { value?: AstNode }).value,
|
|
750
|
+
into
|
|
751
|
+
);
|
|
752
|
+
}
|
|
753
|
+
};
|
|
754
|
+
|
|
755
|
+
const parameterBindingNames = (node: AstNode): readonly string[] => {
|
|
756
|
+
if (!lexicalScopeTypes.has(node.type) || node.type === 'BlockStatement') {
|
|
757
|
+
return [];
|
|
758
|
+
}
|
|
759
|
+
const names = new Set<string>();
|
|
760
|
+
const params =
|
|
761
|
+
(node as unknown as { params?: readonly AstNode[] }).params ?? [];
|
|
762
|
+
for (const param of params) {
|
|
763
|
+
addPatternBindingNames(param, names);
|
|
764
|
+
}
|
|
765
|
+
return [...names];
|
|
766
|
+
};
|
|
767
|
+
|
|
768
|
+
const variableInitializer = (node: AstNode): AstNode | undefined =>
|
|
769
|
+
node.type === 'VariableDeclarator'
|
|
770
|
+
? ((node as unknown as { init?: AstNode }).init ?? undefined)
|
|
771
|
+
: undefined;
|
|
772
|
+
|
|
773
|
+
const isZodSchemaExpression = (
|
|
774
|
+
node: AstNode | undefined,
|
|
775
|
+
schemaBindings: SchemaBindings
|
|
776
|
+
): boolean =>
|
|
777
|
+
node?.type === 'CallExpression' &&
|
|
778
|
+
isZodSchemaCallee(callCallee(node), schemaBindings);
|
|
779
|
+
|
|
780
|
+
const schemaBindingExpressionInitializer = (
|
|
781
|
+
node: AstNode | undefined,
|
|
782
|
+
schemaBindings: SchemaBindings
|
|
783
|
+
): AstNode | undefined => {
|
|
784
|
+
if (isZodSchemaExpression(node, schemaBindings)) {
|
|
785
|
+
return node;
|
|
786
|
+
}
|
|
787
|
+
const name = identifierName(node);
|
|
788
|
+
return name === undefined || node === undefined
|
|
789
|
+
? undefined
|
|
790
|
+
: schemaBindingInitializer(schemaBindings, name, node.start);
|
|
791
|
+
};
|
|
792
|
+
|
|
793
|
+
const astChildNodes = (node: AstNode): readonly AstNode[] => {
|
|
794
|
+
const children: AstNode[] = [];
|
|
795
|
+
for (const value of Object.values(node)) {
|
|
796
|
+
if (Array.isArray(value)) {
|
|
797
|
+
children.push(
|
|
798
|
+
...value.filter(
|
|
799
|
+
(entry): entry is AstNode =>
|
|
800
|
+
typeof entry === 'object' &&
|
|
801
|
+
entry !== null &&
|
|
802
|
+
typeof (entry as AstNode).type === 'string'
|
|
803
|
+
)
|
|
804
|
+
);
|
|
805
|
+
continue;
|
|
806
|
+
}
|
|
807
|
+
if (
|
|
808
|
+
typeof value === 'object' &&
|
|
809
|
+
value !== null &&
|
|
810
|
+
typeof (value as AstNode).type === 'string'
|
|
811
|
+
) {
|
|
812
|
+
children.push(value as AstNode);
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
return children;
|
|
816
|
+
};
|
|
817
|
+
|
|
818
|
+
const collectZodSchemaBindings = (ast: AstNode): SchemaBindings => {
|
|
819
|
+
const bindings = new Map<string, SchemaBindingRecord[]>();
|
|
820
|
+
|
|
821
|
+
const visit = (
|
|
822
|
+
node: AstNode,
|
|
823
|
+
scope: { readonly end: number; readonly start: number }
|
|
824
|
+
): void => {
|
|
825
|
+
const nextScope = lexicalScopeTypes.has(node.type)
|
|
826
|
+
? { end: node.end, start: node.start }
|
|
827
|
+
: scope;
|
|
828
|
+
const name = bindingName(node);
|
|
829
|
+
const initializer = variableInitializer(node);
|
|
830
|
+
for (const parameterName of parameterBindingNames(node)) {
|
|
831
|
+
const records = bindings.get(parameterName) ?? [];
|
|
832
|
+
records.push({
|
|
833
|
+
initializer: undefined,
|
|
834
|
+
scopeEnd: nextScope.end,
|
|
835
|
+
scopeStart: nextScope.start,
|
|
836
|
+
start: node.start,
|
|
837
|
+
});
|
|
838
|
+
bindings.set(parameterName, records);
|
|
839
|
+
}
|
|
840
|
+
if (name !== undefined) {
|
|
841
|
+
const records = bindings.get(name) ?? [];
|
|
842
|
+
records.push({
|
|
843
|
+
initializer: schemaBindingExpressionInitializer(initializer, bindings),
|
|
844
|
+
scopeEnd: nextScope.end,
|
|
845
|
+
scopeStart: nextScope.start,
|
|
846
|
+
start: node.start,
|
|
847
|
+
});
|
|
848
|
+
bindings.set(name, records);
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
for (const child of astChildNodes(node)) {
|
|
852
|
+
visit(child, nextScope);
|
|
853
|
+
}
|
|
854
|
+
};
|
|
855
|
+
|
|
856
|
+
visit(ast, { end: ast.end, start: ast.start });
|
|
857
|
+
return bindings;
|
|
858
|
+
};
|
|
859
|
+
|
|
860
|
+
/**
|
|
861
|
+
* Detect coerced primitive schema calls such as `z.coerce.number()`. The final
|
|
862
|
+
* callee property is a supported primitive name, so the deny-list never matches;
|
|
863
|
+
* the coercion lives on the intermediate `.coerce` member. The runtime marker
|
|
864
|
+
* guard rejects `def.coerce === true`, so Warden must flag the same shape.
|
|
865
|
+
*/
|
|
866
|
+
const isCoerceMarkerCall = (node: AstNode): boolean => {
|
|
867
|
+
if (node.type !== 'CallExpression') {
|
|
868
|
+
return false;
|
|
869
|
+
}
|
|
870
|
+
const callee = callCallee(node);
|
|
871
|
+
if (!callee || !isMemberAccessNonComputed(callee)) {
|
|
872
|
+
return false;
|
|
873
|
+
}
|
|
874
|
+
const object = memberObject(callee);
|
|
875
|
+
return (
|
|
876
|
+
object !== undefined &&
|
|
877
|
+
isMemberAccessNonComputed(object) &&
|
|
878
|
+
memberPropertyName(object) === 'coerce' &&
|
|
879
|
+
isZodSchemaReceiver(memberObject(object))
|
|
880
|
+
);
|
|
881
|
+
};
|
|
882
|
+
|
|
883
|
+
const hasVersionOption = (node: AstNode | undefined): boolean =>
|
|
884
|
+
node?.type === 'ObjectExpression' && hasProperty(node, 'version');
|
|
885
|
+
|
|
886
|
+
const composeCallHasVersionPin = (node: AstNode): boolean => {
|
|
887
|
+
if (node.type !== 'CallExpression') {
|
|
888
|
+
return false;
|
|
889
|
+
}
|
|
890
|
+
const { arguments: args, callee } = node as unknown as {
|
|
891
|
+
arguments?: readonly AstNode[];
|
|
892
|
+
callee?: AstNode;
|
|
893
|
+
};
|
|
894
|
+
if (!callee || !args) {
|
|
895
|
+
return false;
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
const isComposeIdentifier =
|
|
899
|
+
callee.type === 'Identifier' &&
|
|
900
|
+
(callee as unknown as { name?: string }).name === 'compose';
|
|
901
|
+
const { property } = callee as unknown as { property?: AstNode };
|
|
902
|
+
const isComposeMember =
|
|
903
|
+
isMemberAccessNonComputed(callee) &&
|
|
904
|
+
property?.type === 'Identifier' &&
|
|
905
|
+
(property as unknown as { name?: string }).name === 'compose';
|
|
906
|
+
|
|
907
|
+
return (isComposeIdentifier || isComposeMember) && hasVersionOption(args[2]);
|
|
908
|
+
};
|
|
909
|
+
|
|
910
|
+
export const versionPinnedCompose: WardenRule = {
|
|
911
|
+
check(sourceCode, filePath) {
|
|
912
|
+
const ast = parse(filePath, sourceCode);
|
|
913
|
+
if (!ast) {
|
|
914
|
+
return [];
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
const diagnostics: WardenDiagnostic[] = [];
|
|
918
|
+
for (const blaze of findBlazeBodies(ast)) {
|
|
919
|
+
walk(blaze, (node) => {
|
|
920
|
+
if (!composeCallHasVersionPin(node)) {
|
|
921
|
+
return;
|
|
922
|
+
}
|
|
923
|
+
diagnostics.push(
|
|
924
|
+
diagnostic(
|
|
925
|
+
VERSION_PINNED_COMPOSE,
|
|
926
|
+
'warn',
|
|
927
|
+
filePath,
|
|
928
|
+
sourceCode,
|
|
929
|
+
node,
|
|
930
|
+
'ctx.compose() version pins are temporary migration debt. Prefer keeping composition current, or document why this pin can be removed later.'
|
|
931
|
+
)
|
|
932
|
+
);
|
|
933
|
+
});
|
|
934
|
+
}
|
|
935
|
+
return diagnostics;
|
|
936
|
+
},
|
|
937
|
+
description:
|
|
938
|
+
'Warn when ctx.compose() calls pin a specific trail version instead of composing with the current trail.',
|
|
939
|
+
name: VERSION_PINNED_COMPOSE,
|
|
940
|
+
severity: 'warn',
|
|
941
|
+
};
|
|
942
|
+
|
|
943
|
+
export const forkWithoutPreservedBlaze: WardenRule = {
|
|
944
|
+
check(sourceCode, filePath) {
|
|
945
|
+
const ast = parse(filePath, sourceCode);
|
|
946
|
+
if (!ast) {
|
|
947
|
+
return [];
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
const diagnostics: WardenDiagnostic[] = [];
|
|
951
|
+
for (const definition of findTrailDefinitions(ast)) {
|
|
952
|
+
if (definition.kind !== 'trail') {
|
|
953
|
+
continue;
|
|
954
|
+
}
|
|
955
|
+
for (const entry of versionEntries(definition.config)) {
|
|
956
|
+
if (hasProperty(entry, 'transpose') || hasProperty(entry, 'blaze')) {
|
|
957
|
+
continue;
|
|
958
|
+
}
|
|
959
|
+
diagnostics.push(
|
|
960
|
+
diagnostic(
|
|
961
|
+
FORK_WITHOUT_PRESERVED_BLAZE,
|
|
962
|
+
'error',
|
|
963
|
+
filePath,
|
|
964
|
+
sourceCode,
|
|
965
|
+
entry,
|
|
966
|
+
`Trail "${definition.id}" has a historical version entry without transpose or blaze. Add transpose for a revision entry, or preserve the historical blaze for a fork entry.`
|
|
967
|
+
)
|
|
968
|
+
);
|
|
969
|
+
}
|
|
970
|
+
}
|
|
971
|
+
return diagnostics;
|
|
972
|
+
},
|
|
973
|
+
description:
|
|
974
|
+
'Require historical fork version entries to preserve a blaze, while revision entries declare transpose.',
|
|
975
|
+
name: FORK_WITHOUT_PRESERVED_BLAZE,
|
|
976
|
+
severity: 'error',
|
|
977
|
+
};
|
|
978
|
+
|
|
979
|
+
const directSchemaNodesForTrail = (config: AstNode): readonly AstNode[] => {
|
|
980
|
+
const nodes: AstNode[] = [];
|
|
981
|
+
for (const key of ['input', 'output']) {
|
|
982
|
+
const value = propertyValue(findConfigProperty(config, key));
|
|
983
|
+
if (value) {
|
|
984
|
+
nodes.push(value);
|
|
985
|
+
}
|
|
986
|
+
}
|
|
987
|
+
for (const entry of versionEntries(config)) {
|
|
988
|
+
for (const key of ['input', 'output']) {
|
|
989
|
+
const value = propertyValue(findConfigProperty(entry, key));
|
|
990
|
+
if (value) {
|
|
991
|
+
nodes.push(value);
|
|
992
|
+
}
|
|
993
|
+
}
|
|
994
|
+
}
|
|
995
|
+
return nodes;
|
|
996
|
+
};
|
|
997
|
+
|
|
998
|
+
const schemaNodesForTrail = (
|
|
999
|
+
config: AstNode,
|
|
1000
|
+
schemaBindings: SchemaBindings
|
|
1001
|
+
): readonly AstNode[] => {
|
|
1002
|
+
const nodes: AstNode[] = [...directSchemaNodesForTrail(config)];
|
|
1003
|
+
const seenBindings = new Set<string>();
|
|
1004
|
+
let index = 0;
|
|
1005
|
+
while (index < nodes.length) {
|
|
1006
|
+
const node = nodes[index];
|
|
1007
|
+
index += 1;
|
|
1008
|
+
if (node === undefined) {
|
|
1009
|
+
continue;
|
|
1010
|
+
}
|
|
1011
|
+
walkScope(node, (candidate) => {
|
|
1012
|
+
const name = identifierName(candidate);
|
|
1013
|
+
if (name === undefined || seenBindings.has(name)) {
|
|
1014
|
+
return;
|
|
1015
|
+
}
|
|
1016
|
+
const initializer = schemaBindingInitializer(
|
|
1017
|
+
schemaBindings,
|
|
1018
|
+
name,
|
|
1019
|
+
candidate.start
|
|
1020
|
+
);
|
|
1021
|
+
if (initializer === undefined) {
|
|
1022
|
+
return;
|
|
1023
|
+
}
|
|
1024
|
+
seenBindings.add(name);
|
|
1025
|
+
nodes.push(initializer);
|
|
1026
|
+
});
|
|
1027
|
+
}
|
|
1028
|
+
return nodes;
|
|
1029
|
+
};
|
|
1030
|
+
|
|
1031
|
+
export const markerSchemaUnsupported: WardenRule = {
|
|
1032
|
+
check(sourceCode, filePath) {
|
|
1033
|
+
const ast = parse(filePath, sourceCode);
|
|
1034
|
+
if (!ast) {
|
|
1035
|
+
return [];
|
|
1036
|
+
}
|
|
1037
|
+
|
|
1038
|
+
const diagnostics: WardenDiagnostic[] = [];
|
|
1039
|
+
const schemaBindings = collectZodSchemaBindings(ast);
|
|
1040
|
+
for (const definition of findTrailDefinitions(ast)) {
|
|
1041
|
+
if (definition.kind !== 'trail' || !trailIsVersioned(definition.config)) {
|
|
1042
|
+
continue;
|
|
1043
|
+
}
|
|
1044
|
+
const seenDiagnostics = new Set<number>();
|
|
1045
|
+
const unsupportedOptionalWrapperStarts = new Set<number>();
|
|
1046
|
+
for (const schema of directSchemaNodesForTrail(definition.config)) {
|
|
1047
|
+
collectUnsupportedOptionalWrapperStarts(
|
|
1048
|
+
schema,
|
|
1049
|
+
schemaBindings,
|
|
1050
|
+
unsupportedOptionalWrapperStarts
|
|
1051
|
+
);
|
|
1052
|
+
}
|
|
1053
|
+
for (const schema of schemaNodesForTrail(
|
|
1054
|
+
definition.config,
|
|
1055
|
+
schemaBindings
|
|
1056
|
+
)) {
|
|
1057
|
+
walkScope(schema, (node) => {
|
|
1058
|
+
if (
|
|
1059
|
+
!unsupportedOptionalWrapperStarts.has(node.start) &&
|
|
1060
|
+
!isMemberCallNamed(node, unsupportedSchemaCalls, schemaBindings) &&
|
|
1061
|
+
!isCoerceMarkerCall(node) &&
|
|
1062
|
+
!isMultiValueLiteralCall(node, schemaBindings) &&
|
|
1063
|
+
!isJsonLossyLiteralCall(node, schemaBindings) &&
|
|
1064
|
+
!isJsonLossyEnumCall(node, schemaBindings) &&
|
|
1065
|
+
!isReferenceValuedLiteralCall(node, schemaBindings) &&
|
|
1066
|
+
!isReferenceValuedEnumCall(node, schemaBindings) &&
|
|
1067
|
+
!isHiddenOptionalWrapperCall(node, schemaBindings)
|
|
1068
|
+
) {
|
|
1069
|
+
return;
|
|
1070
|
+
}
|
|
1071
|
+
if (seenDiagnostics.has(node.start)) {
|
|
1072
|
+
return;
|
|
1073
|
+
}
|
|
1074
|
+
seenDiagnostics.add(node.start);
|
|
1075
|
+
diagnostics.push(
|
|
1076
|
+
diagnostic(
|
|
1077
|
+
MARKER_SCHEMA_UNSUPPORTED,
|
|
1078
|
+
'error',
|
|
1079
|
+
filePath,
|
|
1080
|
+
sourceCode,
|
|
1081
|
+
node,
|
|
1082
|
+
`Trail "${definition.id}" uses a schema construct outside the supported version-marker subset. Use explicit object, primitive, enum, array, optional, nullable, and union schemas for versioned contracts.`
|
|
1083
|
+
)
|
|
1084
|
+
);
|
|
1085
|
+
});
|
|
1086
|
+
}
|
|
1087
|
+
}
|
|
1088
|
+
return diagnostics;
|
|
1089
|
+
},
|
|
1090
|
+
description:
|
|
1091
|
+
'Reject versioned trail schema constructs that cannot be projected into stable marker contracts.',
|
|
1092
|
+
name: MARKER_SCHEMA_UNSUPPORTED,
|
|
1093
|
+
severity: 'error',
|
|
1094
|
+
};
|