@ontrails/warden 1.0.0-beta.39 → 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 +125 -0
- package/README.md +5 -5
- package/package.json +10 -12
- package/src/cli.ts +49 -44
- package/src/command.ts +2 -2
- package/src/drift.ts +4 -3
- package/src/index.ts +3 -2
- package/src/project-context.ts +3 -3
- package/src/resolve.ts +2 -2
- package/src/rules/captured-kernel.ts +375 -0
- package/src/rules/circular-refs.ts +12 -16
- package/src/rules/cli-command-route-coherence.ts +1 -1
- package/src/rules/composes-declarations.ts +15 -13
- package/src/rules/context-no-surface-types.ts +4 -3
- package/src/rules/dead-internal-trail.ts +3 -3
- package/src/rules/dead-public-trail.ts +3 -3
- package/src/rules/draft-file-marking.ts +4 -9
- package/src/rules/draft-visible-debt.ts +3 -8
- package/src/rules/duplicate-public-contract.ts +50 -5
- package/src/rules/{contour-exists.ts → entity-exists.ts} +67 -64
- package/src/rules/entity-ids.ts +15 -0
- package/src/rules/error-mapping-completeness.ts +2 -2
- package/src/rules/example-valid.ts +84 -84
- package/src/rules/fires-declarations.ts +27 -22
- package/src/rules/governed-symbol-residue.ts +339 -32
- package/src/rules/implementation-returns-result.ts +8 -8
- package/src/rules/incomplete-accessor-for-standard-op.ts +15 -15
- package/src/rules/incomplete-crud.ts +42 -40
- package/src/rules/index.ts +9 -6
- package/src/rules/intent-propagation.ts +3 -5
- package/src/rules/layer-field-name-drift.ts +2 -2
- package/src/rules/library-projection-coherence.ts +2 -2
- package/src/rules/metadata.ts +37 -15
- package/src/rules/missing-reconcile.ts +3 -4
- package/src/rules/missing-visibility.ts +2 -1
- package/src/rules/no-destructured-compose.ts +14 -12
- package/src/rules/no-direct-implementation-call.ts +12 -7
- package/src/rules/no-legacy-cli-alias-export.ts +2 -2
- package/src/rules/no-native-error-result.ts +2 -2
- package/src/rules/no-redundant-result-error-wrap.ts +10 -10
- package/src/rules/no-sync-result-assumption.ts +84 -78
- package/src/rules/no-throw-in-detour-recover.ts +2 -2
- package/src/rules/no-throw-in-implementation.ts +13 -7
- package/src/rules/no-top-level-surface.ts +2 -2
- package/src/rules/on-references-exist.ts +3 -3
- package/src/rules/orphaned-signal.ts +4 -5
- package/src/rules/owner-projection-parity.ts +2 -2
- package/src/rules/public-export-example-coverage.ts +10 -4
- package/src/rules/public-internal-deep-imports.ts +2 -3
- package/src/rules/read-intent-fires.ts +4 -3
- package/src/rules/reference-exists.ts +20 -21
- package/src/rules/registry-names.ts +6 -4
- package/src/rules/resolved-import-boundary.ts +1 -1
- package/src/rules/resource-declarations.ts +21 -17
- package/src/rules/resource-exists.ts +4 -2
- package/src/rules/resource-id-grammar.ts +2 -2
- package/src/rules/resource-mock-coverage.ts +2 -2
- package/src/rules/retired-vocabulary.ts +242 -5
- 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/static-resource-accessor-preference.ts +15 -11
- package/src/rules/surface-overlay-coherence.ts +1 -1
- package/src/rules/surface-trailhead-coherence.ts +2 -2
- package/src/rules/trail-fork-coaching.ts +27 -18
- package/src/rules/trail-versioning-source.ts +16 -12
- package/src/rules/trail-versioning-topo.ts +2 -2
- package/src/rules/trailhead-override-divergence.ts +2 -2
- package/src/rules/types.ts +7 -7
- package/src/rules/unmaterialized-activation-source.ts +1 -0
- package/src/rules/unreachable-detour-shadowing.ts +2 -2
- package/src/rules/valid-describe-refs.ts +2 -2
- package/src/rules/warden-export-symmetry.ts +3 -3
- package/src/rules/warden-rules-use-ast.ts +12 -17
- package/src/trails/activation-orphan.trail.ts +5 -5
- package/src/trails/captured-kernel.trail.ts +108 -0
- package/src/trails/circular-refs.trail.ts +6 -6
- package/src/trails/cli-command-route-coherence.trail.ts +2 -2
- package/src/trails/composes-declarations.trail.ts +1 -1
- package/src/trails/context-no-surface-types.trail.ts +1 -1
- package/src/trails/dead-internal-trail.trail.ts +2 -2
- package/src/trails/dead-public-trail.trail.ts +1 -1
- package/src/trails/deprecation-without-guidance.trail.ts +1 -1
- package/src/trails/duplicate-public-contract.trail.ts +3 -3
- package/src/trails/entity-exists.trail.ts +21 -0
- package/src/trails/example-valid.trail.ts +3 -3
- package/src/trails/fires-declarations.trail.ts +1 -1
- package/src/trails/{fork-without-preserved-blaze.trail.ts → fork-without-preserved-implementation.trail.ts} +4 -4
- package/src/trails/implementation-returns-result.trail.ts +1 -1
- package/src/trails/incomplete-accessor-for-standard-op.trail.ts +4 -4
- package/src/trails/index.ts +3 -2
- package/src/trails/intent-propagation.trail.ts +2 -2
- package/src/trails/library-projection-coherence.trail.ts +2 -2
- package/src/trails/marker-schema-unsupported.trail.ts +1 -1
- package/src/trails/missing-visibility.trail.ts +1 -1
- package/src/trails/no-destructured-compose.trail.ts +5 -5
- package/src/trails/no-direct-implementation-call.trail.ts +1 -1
- package/src/trails/no-redundant-result-error-wrap.trail.ts +2 -2
- package/src/trails/no-sync-result-assumption.trail.ts +2 -2
- package/src/trails/no-throw-in-detour-recover.trail.ts +1 -1
- package/src/trails/no-throw-in-implementation.trail.ts +1 -1
- package/src/trails/on-references-exist.trail.ts +1 -1
- package/src/trails/pending-force.trail.ts +1 -1
- package/src/trails/permit-governance.trail.ts +2 -2
- package/src/trails/prefer-schema-inference.trail.ts +1 -1
- package/src/trails/public-output-schema.trail.ts +2 -2
- package/src/trails/public-union-output-discriminants.trail.ts +1 -1
- package/src/trails/read-intent-fires.trail.ts +1 -1
- package/src/trails/reference-exists.trail.ts +5 -5
- package/src/trails/resource-declarations.trail.ts +1 -1
- package/src/trails/resource-exists.trail.ts +1 -1
- package/src/trails/run.ts +4 -6
- package/src/trails/scheduled-destroy-intent.trail.ts +2 -2
- package/src/trails/schema.ts +9 -9
- package/src/trails/signal-graph-coaching.trail.ts +2 -2
- package/src/trails/static-resource-accessor-preference.trail.ts +1 -1
- package/src/trails/surface-overlay-coherence.trail.ts +1 -1
- package/src/trails/trail-fork-coaching.trail.ts +1 -1
- package/src/trails/unmaterialized-activation-source.trail.ts +5 -5
- package/src/trails/valid-detour-contract.trail.ts +1 -1
- package/src/trails/version-gap.trail.ts +1 -1
- package/src/trails/version-pinned-compose.trail.ts +1 -1
- package/src/trails/version-without-examples.trail.ts +1 -1
- package/src/trails/warden-rules-use-ast.trail.ts +2 -2
- package/src/trails/webhook-route-collision.trail.ts +2 -2
- package/src/trails/wrap-rule.ts +17 -17
- package/src/ast.ts +0 -137
- package/src/rules/ast.ts +0 -4143
- package/src/rules/contour-ids.ts +0 -15
- package/src/trails/contour-exists.trail.ts +0 -21
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { entity } from '@ontrails/core';
|
|
2
2
|
import { z } from 'zod';
|
|
3
3
|
|
|
4
4
|
import {
|
|
5
5
|
extractStringLiteral,
|
|
6
6
|
findConfigProperty,
|
|
7
|
-
|
|
7
|
+
findEntityDefinitions,
|
|
8
8
|
getNodeExpression,
|
|
9
9
|
getNodeObject,
|
|
10
10
|
getNodeProperty,
|
|
@@ -12,24 +12,24 @@ import {
|
|
|
12
12
|
identifierName,
|
|
13
13
|
offsetToLine,
|
|
14
14
|
parse,
|
|
15
|
-
} from '
|
|
16
|
-
import type { AstNode,
|
|
15
|
+
} from '@ontrails/source';
|
|
16
|
+
import type { AstNode, EntityDefinition } from '@ontrails/source';
|
|
17
17
|
import { isTestFile } from './scan.js';
|
|
18
18
|
import type { WardenDiagnostic, WardenRule } from './types.js';
|
|
19
19
|
|
|
20
|
-
class
|
|
20
|
+
class UnsupportedEntityEvaluationError extends Error {}
|
|
21
21
|
|
|
22
|
-
type
|
|
22
|
+
type EntityEvaluationEnvironment = ReadonlyMap<string, unknown>;
|
|
23
23
|
|
|
24
24
|
const buildInvalidExampleDiagnostic = (
|
|
25
|
-
|
|
25
|
+
entityName: string,
|
|
26
26
|
message: string,
|
|
27
27
|
filePath: string,
|
|
28
28
|
line: number
|
|
29
29
|
): WardenDiagnostic => ({
|
|
30
30
|
filePath,
|
|
31
31
|
line,
|
|
32
|
-
message: `
|
|
32
|
+
message: `Entity "${entityName}" has invalid examples: ${message}`,
|
|
33
33
|
rule: 'example-valid',
|
|
34
34
|
severity: 'error',
|
|
35
35
|
});
|
|
@@ -49,53 +49,53 @@ const getPropertyName = (node: unknown): string | null => {
|
|
|
49
49
|
|
|
50
50
|
const requireNode = (node: AstNode | undefined): AstNode => {
|
|
51
51
|
if (!node) {
|
|
52
|
-
throw new
|
|
53
|
-
'Missing node in
|
|
52
|
+
throw new UnsupportedEntityEvaluationError(
|
|
53
|
+
'Missing node in entity evaluation.'
|
|
54
54
|
);
|
|
55
55
|
}
|
|
56
56
|
|
|
57
57
|
return node;
|
|
58
58
|
};
|
|
59
59
|
|
|
60
|
-
type
|
|
60
|
+
type EntityNodeEvaluator = (
|
|
61
61
|
node: AstNode,
|
|
62
|
-
env:
|
|
62
|
+
env: EntityEvaluationEnvironment
|
|
63
63
|
) => unknown;
|
|
64
64
|
|
|
65
65
|
// The evaluator table is declared before `evaluateNode` so the recursive
|
|
66
66
|
// dispatch can read it at call time without any forward references. Entries
|
|
67
67
|
// are registered below once each per-type evaluator is defined.
|
|
68
|
-
const
|
|
68
|
+
const entityNodeEvaluators = new Map<string, EntityNodeEvaluator>();
|
|
69
69
|
|
|
70
|
-
const evaluateNode:
|
|
70
|
+
const evaluateNode: EntityNodeEvaluator = (
|
|
71
71
|
node: AstNode,
|
|
72
|
-
env:
|
|
72
|
+
env: EntityEvaluationEnvironment
|
|
73
73
|
): unknown => {
|
|
74
|
-
const evaluator =
|
|
74
|
+
const evaluator = entityNodeEvaluators.get(node.type);
|
|
75
75
|
if (evaluator) {
|
|
76
76
|
return evaluator(node, env);
|
|
77
77
|
}
|
|
78
78
|
|
|
79
|
-
throw new
|
|
80
|
-
`Unsupported AST node "${node.type}" in
|
|
79
|
+
throw new UnsupportedEntityEvaluationError(
|
|
80
|
+
`Unsupported AST node "${node.type}" in entity evaluation.`
|
|
81
81
|
);
|
|
82
82
|
};
|
|
83
83
|
|
|
84
|
-
const evaluateArrayExpression:
|
|
84
|
+
const evaluateArrayExpression: EntityNodeEvaluator = (
|
|
85
85
|
node: AstNode,
|
|
86
|
-
env:
|
|
86
|
+
env: EntityEvaluationEnvironment
|
|
87
87
|
): readonly unknown[] => {
|
|
88
88
|
const elements = node['elements'] as readonly AstNode[] | undefined;
|
|
89
89
|
return (elements ?? []).map((element) => evaluateNode(element, env));
|
|
90
90
|
};
|
|
91
91
|
|
|
92
|
-
const evaluateLiteralExpression:
|
|
92
|
+
const evaluateLiteralExpression: EntityNodeEvaluator = (
|
|
93
93
|
node: AstNode
|
|
94
94
|
): unknown => getStringValue(node) ?? node.value;
|
|
95
95
|
|
|
96
|
-
const evaluateIdentifierExpression:
|
|
96
|
+
const evaluateIdentifierExpression: EntityNodeEvaluator = (
|
|
97
97
|
node: AstNode,
|
|
98
|
-
env:
|
|
98
|
+
env: EntityEvaluationEnvironment
|
|
99
99
|
): unknown => {
|
|
100
100
|
const name = identifierName(node);
|
|
101
101
|
if (name === 'undefined') {
|
|
@@ -107,39 +107,39 @@ const evaluateIdentifierExpression: ContourNodeEvaluator = (
|
|
|
107
107
|
}
|
|
108
108
|
|
|
109
109
|
if (!name || !env.has(name)) {
|
|
110
|
-
throw new
|
|
111
|
-
`Unknown identifier "${name ?? '<unknown>'}" in
|
|
110
|
+
throw new UnsupportedEntityEvaluationError(
|
|
111
|
+
`Unknown identifier "${name ?? '<unknown>'}" in entity evaluation.`
|
|
112
112
|
);
|
|
113
113
|
}
|
|
114
114
|
|
|
115
115
|
return env.get(name);
|
|
116
116
|
};
|
|
117
117
|
|
|
118
|
-
const evaluateWrappedExpression:
|
|
118
|
+
const evaluateWrappedExpression: EntityNodeEvaluator = (
|
|
119
119
|
node: AstNode,
|
|
120
|
-
env:
|
|
120
|
+
env: EntityEvaluationEnvironment
|
|
121
121
|
): unknown => evaluateNode(requireNode(getNodeExpression(node)), env);
|
|
122
122
|
|
|
123
|
-
const evaluateNullExpression:
|
|
123
|
+
const evaluateNullExpression: EntityNodeEvaluator = (): null => null;
|
|
124
124
|
|
|
125
|
-
const evaluateObjectExpression:
|
|
125
|
+
const evaluateObjectExpression: EntityNodeEvaluator = (
|
|
126
126
|
node: AstNode,
|
|
127
|
-
env:
|
|
127
|
+
env: EntityEvaluationEnvironment
|
|
128
128
|
): Record<string, unknown> => {
|
|
129
129
|
const properties = node['properties'] as readonly AstNode[] | undefined;
|
|
130
130
|
const value: Record<string, unknown> = {};
|
|
131
131
|
|
|
132
132
|
for (const property of properties ?? []) {
|
|
133
133
|
if (property.type !== 'Property') {
|
|
134
|
-
throw new
|
|
134
|
+
throw new UnsupportedEntityEvaluationError(
|
|
135
135
|
`Unsupported object property type "${property.type}".`
|
|
136
136
|
);
|
|
137
137
|
}
|
|
138
138
|
|
|
139
139
|
const propertyName = getPropertyName(property.key);
|
|
140
140
|
if (!propertyName) {
|
|
141
|
-
throw new
|
|
142
|
-
'Unsupported object property key in
|
|
141
|
+
throw new UnsupportedEntityEvaluationError(
|
|
142
|
+
'Unsupported object property key in entity evaluation.'
|
|
143
143
|
);
|
|
144
144
|
}
|
|
145
145
|
|
|
@@ -154,107 +154,107 @@ const evaluateObjectExpression: ContourNodeEvaluator = (
|
|
|
154
154
|
|
|
155
155
|
const evaluateCallArguments = (
|
|
156
156
|
node: AstNode,
|
|
157
|
-
env:
|
|
157
|
+
env: EntityEvaluationEnvironment
|
|
158
158
|
): readonly unknown[] =>
|
|
159
159
|
((node['arguments'] as readonly AstNode[] | undefined) ?? []).map((arg) =>
|
|
160
160
|
evaluateNode(arg, env)
|
|
161
161
|
);
|
|
162
162
|
|
|
163
|
-
const
|
|
163
|
+
const resolveEntityFallbackOptions = (
|
|
164
164
|
shape: unknown
|
|
165
|
-
): Parameters<typeof
|
|
165
|
+
): Parameters<typeof entity>[2] | null =>
|
|
166
166
|
typeof shape === 'object' &&
|
|
167
167
|
shape !== null &&
|
|
168
168
|
Object.hasOwn(shape, 'id') &&
|
|
169
169
|
(shape as z.ZodRawShape)['id'] !== undefined
|
|
170
|
-
? ({ identity: 'id' } as Parameters<typeof
|
|
170
|
+
? ({ identity: 'id' } as Parameters<typeof entity>[2])
|
|
171
171
|
: null;
|
|
172
172
|
|
|
173
|
-
const
|
|
173
|
+
const resolveEntityOptions = (
|
|
174
174
|
shape: unknown,
|
|
175
175
|
options: unknown
|
|
176
|
-
): Parameters<typeof
|
|
176
|
+
): Parameters<typeof entity>[2] => {
|
|
177
177
|
if (options === undefined) {
|
|
178
|
-
const fallbackOptions =
|
|
178
|
+
const fallbackOptions = resolveEntityFallbackOptions(shape);
|
|
179
179
|
if (fallbackOptions !== null) {
|
|
180
180
|
return fallbackOptions;
|
|
181
181
|
}
|
|
182
182
|
|
|
183
|
-
throw new
|
|
184
|
-
'
|
|
183
|
+
throw new UnsupportedEntityEvaluationError(
|
|
184
|
+
'Entity evaluator requires literal options, or an `id` field when options are omitted.'
|
|
185
185
|
);
|
|
186
186
|
}
|
|
187
187
|
|
|
188
188
|
if (typeof options !== 'object' || options === null) {
|
|
189
|
-
throw new
|
|
190
|
-
'
|
|
189
|
+
throw new UnsupportedEntityEvaluationError(
|
|
190
|
+
'Entity evaluator requires literal options when provided.'
|
|
191
191
|
);
|
|
192
192
|
}
|
|
193
193
|
|
|
194
|
-
return options as Parameters<typeof
|
|
194
|
+
return options as Parameters<typeof entity>[2];
|
|
195
195
|
};
|
|
196
196
|
|
|
197
|
-
const
|
|
197
|
+
const evaluateEntityCall = (args: readonly unknown[]): unknown => {
|
|
198
198
|
const [name, shape, options] = args;
|
|
199
199
|
if (typeof name !== 'string') {
|
|
200
|
-
throw new
|
|
201
|
-
'
|
|
200
|
+
throw new UnsupportedEntityEvaluationError(
|
|
201
|
+
'Entity evaluator requires a literal name.'
|
|
202
202
|
);
|
|
203
203
|
}
|
|
204
204
|
|
|
205
|
-
return
|
|
205
|
+
return entity(
|
|
206
206
|
name,
|
|
207
207
|
shape as z.ZodRawShape,
|
|
208
|
-
|
|
208
|
+
resolveEntityOptions(shape, options)
|
|
209
209
|
);
|
|
210
210
|
};
|
|
211
211
|
|
|
212
212
|
const evaluateMemberCall = (
|
|
213
213
|
callee: AstNode,
|
|
214
214
|
args: readonly unknown[],
|
|
215
|
-
env:
|
|
215
|
+
env: EntityEvaluationEnvironment
|
|
216
216
|
): unknown => {
|
|
217
217
|
const receiver = evaluateNode(requireNode(getNodeObject(callee)), env);
|
|
218
218
|
const propertyName = getPropertyName(getNodeProperty(callee));
|
|
219
219
|
if (!propertyName) {
|
|
220
|
-
throw new
|
|
221
|
-
'Unsupported member property in
|
|
220
|
+
throw new UnsupportedEntityEvaluationError(
|
|
221
|
+
'Unsupported member property in entity evaluation.'
|
|
222
222
|
);
|
|
223
223
|
}
|
|
224
224
|
|
|
225
225
|
const method = (receiver as Record<string, unknown>)[propertyName];
|
|
226
226
|
if (typeof method !== 'function') {
|
|
227
|
-
throw new
|
|
228
|
-
`
|
|
227
|
+
throw new UnsupportedEntityEvaluationError(
|
|
228
|
+
`Entity evaluator could not call "${propertyName}".`
|
|
229
229
|
);
|
|
230
230
|
}
|
|
231
231
|
|
|
232
232
|
return Reflect.apply(method, receiver, args);
|
|
233
233
|
};
|
|
234
234
|
|
|
235
|
-
const evaluateCallExpression:
|
|
235
|
+
const evaluateCallExpression: EntityNodeEvaluator = (
|
|
236
236
|
node: AstNode,
|
|
237
|
-
env:
|
|
237
|
+
env: EntityEvaluationEnvironment
|
|
238
238
|
): unknown => {
|
|
239
239
|
const args = evaluateCallArguments(node, env);
|
|
240
240
|
const callee = requireNode(node['callee'] as AstNode | undefined);
|
|
241
241
|
|
|
242
242
|
if (callee.type === 'Identifier') {
|
|
243
243
|
const calleeName = identifierName(callee);
|
|
244
|
-
if (calleeName !== '
|
|
245
|
-
throw new
|
|
246
|
-
`Unsupported
|
|
244
|
+
if (calleeName !== 'entity') {
|
|
245
|
+
throw new UnsupportedEntityEvaluationError(
|
|
246
|
+
`Unsupported entity evaluator call "${calleeName ?? '<unknown>'}".`
|
|
247
247
|
);
|
|
248
248
|
}
|
|
249
249
|
|
|
250
|
-
return
|
|
250
|
+
return evaluateEntityCall(args);
|
|
251
251
|
}
|
|
252
252
|
|
|
253
253
|
if (
|
|
254
254
|
callee.type !== 'MemberExpression' &&
|
|
255
255
|
callee.type !== 'StaticMemberExpression'
|
|
256
256
|
) {
|
|
257
|
-
throw new
|
|
257
|
+
throw new UnsupportedEntityEvaluationError(
|
|
258
258
|
`Unsupported callee type "${callee.type}".`
|
|
259
259
|
);
|
|
260
260
|
}
|
|
@@ -264,7 +264,7 @@ const evaluateCallExpression: ContourNodeEvaluator = (
|
|
|
264
264
|
|
|
265
265
|
const EVALUATOR_REGISTRATIONS: readonly (readonly [
|
|
266
266
|
string,
|
|
267
|
-
|
|
267
|
+
EntityNodeEvaluator,
|
|
268
268
|
])[] = [
|
|
269
269
|
['ArrayExpression', evaluateArrayExpression],
|
|
270
270
|
['BooleanLiteral', evaluateLiteralExpression],
|
|
@@ -281,26 +281,26 @@ const EVALUATOR_REGISTRATIONS: readonly (readonly [
|
|
|
281
281
|
];
|
|
282
282
|
|
|
283
283
|
for (const [type, evaluator] of EVALUATOR_REGISTRATIONS) {
|
|
284
|
-
|
|
284
|
+
entityNodeEvaluators.set(type, evaluator);
|
|
285
285
|
}
|
|
286
286
|
|
|
287
|
-
const hasExamples = (definition:
|
|
287
|
+
const hasExamples = (definition: EntityDefinition): boolean =>
|
|
288
288
|
definition.options !== null &&
|
|
289
289
|
findConfigProperty(definition.options, 'examples') !== null;
|
|
290
290
|
|
|
291
|
-
const
|
|
292
|
-
definition:
|
|
293
|
-
env:
|
|
291
|
+
const evaluateEntityDefinition = (
|
|
292
|
+
definition: EntityDefinition,
|
|
293
|
+
env: EntityEvaluationEnvironment
|
|
294
294
|
): unknown => evaluateNode(definition.call, env);
|
|
295
295
|
|
|
296
|
-
const
|
|
297
|
-
definition:
|
|
296
|
+
const buildEntityExampleErrorDiagnostic = (
|
|
297
|
+
definition: EntityDefinition,
|
|
298
298
|
error: unknown,
|
|
299
299
|
sourceCode: string,
|
|
300
300
|
filePath: string
|
|
301
301
|
): WardenDiagnostic | null => {
|
|
302
302
|
if (
|
|
303
|
-
error instanceof
|
|
303
|
+
error instanceof UnsupportedEntityEvaluationError ||
|
|
304
304
|
!hasExamples(definition) ||
|
|
305
305
|
!(error instanceof Error)
|
|
306
306
|
) {
|
|
@@ -315,21 +315,21 @@ const buildContourExampleErrorDiagnostic = (
|
|
|
315
315
|
);
|
|
316
316
|
};
|
|
317
317
|
|
|
318
|
-
const
|
|
319
|
-
definition:
|
|
318
|
+
const evaluateEntityExamples = (
|
|
319
|
+
definition: EntityDefinition,
|
|
320
320
|
env: Map<string, unknown>,
|
|
321
321
|
sourceCode: string,
|
|
322
322
|
filePath: string
|
|
323
323
|
): WardenDiagnostic | null => {
|
|
324
324
|
try {
|
|
325
|
-
const value =
|
|
325
|
+
const value = evaluateEntityDefinition(definition, env);
|
|
326
326
|
if (definition.bindingName) {
|
|
327
327
|
env.set(definition.bindingName, value);
|
|
328
328
|
}
|
|
329
329
|
|
|
330
330
|
return null;
|
|
331
331
|
} catch (error) {
|
|
332
|
-
return
|
|
332
|
+
return buildEntityExampleErrorDiagnostic(
|
|
333
333
|
definition,
|
|
334
334
|
error,
|
|
335
335
|
sourceCode,
|
|
@@ -338,8 +338,8 @@ const evaluateContourExamples = (
|
|
|
338
338
|
}
|
|
339
339
|
};
|
|
340
340
|
|
|
341
|
-
const
|
|
342
|
-
definitions: readonly
|
|
341
|
+
const collectEntityExampleDiagnostics = (
|
|
342
|
+
definitions: readonly EntityDefinition[],
|
|
343
343
|
sourceCode: string,
|
|
344
344
|
filePath: string
|
|
345
345
|
): readonly WardenDiagnostic[] => {
|
|
@@ -347,7 +347,7 @@ const collectContourExampleDiagnostics = (
|
|
|
347
347
|
const env = new Map<string, unknown>();
|
|
348
348
|
|
|
349
349
|
for (const definition of definitions) {
|
|
350
|
-
const diagnostic =
|
|
350
|
+
const diagnostic = evaluateEntityExamples(
|
|
351
351
|
definition,
|
|
352
352
|
env,
|
|
353
353
|
sourceCode,
|
|
@@ -361,7 +361,7 @@ const collectContourExampleDiagnostics = (
|
|
|
361
361
|
return diagnostics;
|
|
362
362
|
};
|
|
363
363
|
|
|
364
|
-
const
|
|
364
|
+
const checkEntityExamples = (
|
|
365
365
|
sourceCode: string,
|
|
366
366
|
filePath: string
|
|
367
367
|
): readonly WardenDiagnostic[] => {
|
|
@@ -374,22 +374,22 @@ const checkContourExamples = (
|
|
|
374
374
|
return [];
|
|
375
375
|
}
|
|
376
376
|
|
|
377
|
-
return
|
|
378
|
-
|
|
377
|
+
return collectEntityExampleDiagnostics(
|
|
378
|
+
findEntityDefinitions(ast),
|
|
379
379
|
sourceCode,
|
|
380
380
|
filePath
|
|
381
381
|
);
|
|
382
382
|
};
|
|
383
383
|
|
|
384
384
|
/**
|
|
385
|
-
* Checks that
|
|
385
|
+
* Checks that entity examples validate against their schema.
|
|
386
386
|
*/
|
|
387
387
|
export const exampleValid: WardenRule = {
|
|
388
388
|
check(sourceCode: string, filePath: string): readonly WardenDiagnostic[] {
|
|
389
|
-
return
|
|
389
|
+
return checkEntityExamples(sourceCode, filePath);
|
|
390
390
|
},
|
|
391
391
|
description:
|
|
392
|
-
'Ensure every
|
|
392
|
+
'Ensure every entity example validates against the declared entity schema.',
|
|
393
393
|
name: 'example-valid',
|
|
394
394
|
severity: 'error',
|
|
395
395
|
};
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Validates that `ctx.fire()` calls match the declared `fires` array.
|
|
3
3
|
*
|
|
4
|
-
* Statically analyzes trail `
|
|
4
|
+
* Statically analyzes trail `implementation` functions to find `ctx.fire(signal, ...)`
|
|
5
5
|
* calls and compares locally-resolved `Signal` values against the `fires: [...]`
|
|
6
6
|
* declaration in the trail config. Reports errors for undeclared fires, string
|
|
7
7
|
* fire calls that no longer match the public runtime API, and warnings for
|
|
@@ -11,12 +11,12 @@
|
|
|
11
11
|
* shape, same const-identifier resolution, same context-parameter handling.
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
|
+
import { buildSignalIdentifierResolver } from './source/signals.js';
|
|
14
15
|
import {
|
|
15
|
-
buildSignalIdentifierResolver,
|
|
16
16
|
deriveConstString,
|
|
17
17
|
extractStringLiteral,
|
|
18
|
-
findBlazeBodies,
|
|
19
18
|
findConfigProperty,
|
|
19
|
+
findImplementationBodies,
|
|
20
20
|
findTrailDefinitions,
|
|
21
21
|
getNodeBodyNode,
|
|
22
22
|
getNodeBodyStatements,
|
|
@@ -34,8 +34,9 @@ import {
|
|
|
34
34
|
offsetToLine,
|
|
35
35
|
parse,
|
|
36
36
|
walkScope,
|
|
37
|
-
} from '
|
|
38
|
-
import type { AstNode
|
|
37
|
+
} from '@ontrails/source';
|
|
38
|
+
import type { AstNode } from '@ontrails/source';
|
|
39
|
+
import type { SignalIdentifierResolver } from './source/signals.js';
|
|
39
40
|
import { isTestFile } from './scan.js';
|
|
40
41
|
import type { WardenDiagnostic, WardenRule } from './types.js';
|
|
41
42
|
|
|
@@ -164,14 +165,16 @@ const extractMemberPair = (
|
|
|
164
165
|
};
|
|
165
166
|
|
|
166
167
|
/**
|
|
167
|
-
* Extract the second parameter node from a
|
|
168
|
+
* Extract the second parameter node from a implementation function node.
|
|
168
169
|
*
|
|
169
170
|
* Handles `(input, ctx) => ...`, `async (input, context) => ...`,
|
|
170
171
|
* `function(input, ctx) { ... }`, and parameter-level destructuring
|
|
171
172
|
* like `(input, { fire }) => ...`.
|
|
172
173
|
*/
|
|
173
|
-
const extractContextParamNode = (
|
|
174
|
-
|
|
174
|
+
const extractContextParamNode = (
|
|
175
|
+
implementationBody: AstNode
|
|
176
|
+
): AstNode | null => {
|
|
177
|
+
const params = implementationBody['params'] as readonly AstNode[] | undefined;
|
|
175
178
|
if (!params || params.length < 2) {
|
|
176
179
|
return null;
|
|
177
180
|
}
|
|
@@ -212,7 +215,7 @@ const collectFireNamesFromPattern = (
|
|
|
212
215
|
};
|
|
213
216
|
|
|
214
217
|
/**
|
|
215
|
-
* Extract the second parameter name from a
|
|
218
|
+
* Extract the second parameter name from a implementation function node.
|
|
216
219
|
*
|
|
217
220
|
* Returns null when the parameter is not a plain Identifier (e.g. when the
|
|
218
221
|
* author destructures `{ fire }` in the parameter list). Parameter-level
|
|
@@ -222,8 +225,10 @@ const collectFireNamesFromPattern = (
|
|
|
222
225
|
* (AssignmentPattern whose `.left` is the Identifier). Without this, valid
|
|
223
226
|
* signatures would silently drop out of ctx-access analysis.
|
|
224
227
|
*/
|
|
225
|
-
const extractContextParamName = (
|
|
226
|
-
|
|
228
|
+
const extractContextParamName = (
|
|
229
|
+
implementationBody: AstNode
|
|
230
|
+
): string | null => {
|
|
231
|
+
const param = extractContextParamNode(implementationBody);
|
|
227
232
|
if (!param) {
|
|
228
233
|
return null;
|
|
229
234
|
}
|
|
@@ -238,7 +243,7 @@ const extractContextParamName = (blazeBody: AstNode): string | null => {
|
|
|
238
243
|
* Collect `fire` local names bound via parameter-level destructuring.
|
|
239
244
|
*
|
|
240
245
|
* Recognizes `(input, { fire }) => ...` and `(input, { fire: emit }) => ...`.
|
|
241
|
-
* When the
|
|
246
|
+
* When the implementation author destructures in the parameter list, there is no
|
|
242
247
|
* enclosing `ctx` identifier to track — we seed the fire local set directly
|
|
243
248
|
* from the ObjectPattern in `params[1]`.
|
|
244
249
|
*/
|
|
@@ -350,7 +355,7 @@ const extractFireCallId = (
|
|
|
350
355
|
};
|
|
351
356
|
|
|
352
357
|
/**
|
|
353
|
-
* Walk a
|
|
358
|
+
* Walk a implementation body and collect local names bound to `ctx.fire` via destructure.
|
|
354
359
|
*
|
|
355
360
|
* Recognizes:
|
|
356
361
|
* - `const { fire } = ctx;` → adds `fire`
|
|
@@ -382,8 +387,8 @@ const getCtxDestructurePattern = (
|
|
|
382
387
|
|
|
383
388
|
/**
|
|
384
389
|
* Collect `fire` local names destructured from ctx at the TOP LEVEL of the
|
|
385
|
-
*
|
|
386
|
-
* to avoid leaking nested-scope bindings into the outer
|
|
390
|
+
* implementation body. Destructures inside nested functions are intentionally ignored
|
|
391
|
+
* to avoid leaking nested-scope bindings into the outer implementation scope — a
|
|
387
392
|
* `const { fire } = ctx` inside a nested helper should not cause an outer
|
|
388
393
|
* bare `fire('x')` to be treated as a ctx-bound call.
|
|
389
394
|
*
|
|
@@ -398,7 +403,7 @@ const getCtxDestructurePattern = (
|
|
|
398
403
|
* class of false positives. The runtime + signal-id compose-check still
|
|
399
404
|
* validate real undeclared fires.
|
|
400
405
|
*/
|
|
401
|
-
/** Get the top-level statements of a
|
|
406
|
+
/** Get the top-level statements of a implementation function's BlockStatement body. */
|
|
402
407
|
const getTopLevelStatements = (body: AstNode): readonly AstNode[] => {
|
|
403
408
|
const blockBody = getNodeBodyNode(body);
|
|
404
409
|
if (!blockBody || blockBody.type !== 'BlockStatement') {
|
|
@@ -448,10 +453,10 @@ const collectDestructuredFireNames = (
|
|
|
448
453
|
/**
|
|
449
454
|
* Build the set of context parameter names to match against.
|
|
450
455
|
*
|
|
451
|
-
* Returns ONLY the actual second-parameter name from the
|
|
452
|
-
* No seeded defaults: if the
|
|
456
|
+
* Returns ONLY the actual second-parameter name from the implementation signature.
|
|
457
|
+
* No seeded defaults: if the implementation has no second parameter, the returned set
|
|
453
458
|
* is empty and no `ctx.fire(...)` / `context.fire(...)` calls are tracked
|
|
454
|
-
* for that
|
|
459
|
+
* for that implementation. An unrelated closure-scoped `ctx` identifier is not the
|
|
455
460
|
* trail context and must not be treated as one.
|
|
456
461
|
*/
|
|
457
462
|
const buildCtxNames = (body: AstNode): ReadonlySet<string> => {
|
|
@@ -464,7 +469,7 @@ const buildCtxNames = (body: AstNode): ReadonlySet<string> => {
|
|
|
464
469
|
};
|
|
465
470
|
|
|
466
471
|
/**
|
|
467
|
-
* Walk
|
|
472
|
+
* Walk implementation bodies and collect all statically resolvable ctx.fire() signal IDs.
|
|
468
473
|
*
|
|
469
474
|
* Traversal uses `walkScope`, which stops at nested function boundaries
|
|
470
475
|
* (FunctionDeclaration, FunctionExpression, ArrowFunctionExpression). This
|
|
@@ -473,7 +478,7 @@ const buildCtxNames = (body: AstNode): ReadonlySet<string> => {
|
|
|
473
478
|
* destructured `fire` local:
|
|
474
479
|
*
|
|
475
480
|
* ```ts
|
|
476
|
-
*
|
|
481
|
+
* implementation: async (_, ctx) => {
|
|
477
482
|
* const { fire } = ctx;
|
|
478
483
|
* function nested(fire) { fire(orderPlaced); } // ignored — shadowed
|
|
479
484
|
* function other(ctx) { ctx.fire(orderPlaced); } // ignored — shadowed
|
|
@@ -558,7 +563,7 @@ const extractCalledFires = (
|
|
|
558
563
|
const stringIds = new Set<string>();
|
|
559
564
|
const merged = { hasUnresolved: false, ids, stringIds };
|
|
560
565
|
|
|
561
|
-
for (const body of
|
|
566
|
+
for (const body of findImplementationBodies(config)) {
|
|
562
567
|
mergeCalledFires(
|
|
563
568
|
merged,
|
|
564
569
|
extractCalledFiresFromBody(body, sourceCode, signalIds)
|