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