@altopelago/aeos-core 0.9.0 → 0.9.2
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/LICENSE +21 -0
- package/dist/diag/codes.d.ts +4 -0
- package/dist/diag/codes.d.ts.map +1 -1
- package/dist/diag/codes.js +4 -0
- package/dist/diag/codes.js.map +1 -1
- package/dist/rules/numericForm.d.ts +1 -0
- package/dist/rules/numericForm.d.ts.map +1 -1
- package/dist/rules/numericForm.js +106 -18
- package/dist/rules/numericForm.js.map +1 -1
- package/dist/rules/presence.d.ts.map +1 -1
- package/dist/rules/presence.js +2 -0
- package/dist/rules/presence.js.map +1 -1
- package/dist/rules/referenceForm.d.ts.map +1 -1
- package/dist/rules/referenceForm.js +5 -1
- package/dist/rules/referenceForm.js.map +1 -1
- package/dist/rules/schemaIndex.d.ts.map +1 -1
- package/dist/rules/schemaIndex.js +234 -12
- package/dist/rules/schemaIndex.js.map +1 -1
- package/dist/rules/stringForm.d.ts +3 -2
- package/dist/rules/stringForm.d.ts.map +1 -1
- package/dist/rules/stringForm.js +35 -24
- package/dist/rules/stringForm.js.map +1 -1
- package/dist/rules/typeCheck.d.ts.map +1 -1
- package/dist/rules/typeCheck.js +22 -2
- package/dist/rules/typeCheck.js.map +1 -1
- package/dist/types/schema.d.ts +43 -4
- package/dist/types/schema.d.ts.map +1 -1
- package/dist/types/schema.js +11 -0
- package/dist/types/schema.js.map +1 -1
- package/dist/validate.d.ts +3 -0
- package/dist/validate.d.ts.map +1 -1
- package/dist/validate.js +710 -39
- package/dist/validate.js.map +1 -1
- package/package.json +28 -3
package/dist/validate.js
CHANGED
|
@@ -12,7 +12,7 @@ import { checkPresence } from './rules/presence.js';
|
|
|
12
12
|
import { checkTypes } from './rules/typeCheck.js';
|
|
13
13
|
import { checkReferenceForms } from './rules/referenceForm.js';
|
|
14
14
|
import { checkNumericForm } from './rules/numericForm.js';
|
|
15
|
-
import { checkStringForm, checkPatterns } from './rules/stringForm.js';
|
|
15
|
+
import { checkStringForm, checkPatterns, matchesPortablePattern } from './rules/stringForm.js';
|
|
16
16
|
const TYPE_ALIASES = {
|
|
17
17
|
NumberLiteral: ['NumberLiteral'],
|
|
18
18
|
StringLiteral: ['StringLiteral'],
|
|
@@ -22,6 +22,17 @@ const TYPE_ALIASES = {
|
|
|
22
22
|
ListNode: ['ListNode'],
|
|
23
23
|
ListLiteral: ['ListNode', 'ListLiteral'],
|
|
24
24
|
TupleLiteral: ['TupleLiteral'],
|
|
25
|
+
ToggleLiteral: ['ToggleLiteral'],
|
|
26
|
+
InfinityLiteral: ['InfinityLiteral'],
|
|
27
|
+
NaNLiteral: ['NaNLiteral'],
|
|
28
|
+
HexLiteral: ['HexLiteral'],
|
|
29
|
+
RadixLiteral: ['RadixLiteral'],
|
|
30
|
+
EncodingLiteral: ['EncodingLiteral'],
|
|
31
|
+
SeparatorLiteral: ['SeparatorLiteral'],
|
|
32
|
+
DateLiteral: ['DateLiteral'],
|
|
33
|
+
TimeLiteral: ['TimeLiteral'],
|
|
34
|
+
DateTimeLiteral: ['DateTimeLiteral'],
|
|
35
|
+
ZRUTDateTimeLiteral: ['ZRUTDateTimeLiteral'],
|
|
25
36
|
CloneReference: ['CloneReference'],
|
|
26
37
|
PointerReference: ['PointerReference'],
|
|
27
38
|
NodeLiteral: ['NodeLiteral'],
|
|
@@ -29,6 +40,113 @@ const TYPE_ALIASES = {
|
|
|
29
40
|
function formatQuotedMemberSegment(key) {
|
|
30
41
|
return `.[${JSON.stringify(String(key))}]`;
|
|
31
42
|
}
|
|
43
|
+
const DEFAULT_RESOURCE_POLICY = {
|
|
44
|
+
max_events: 100_000,
|
|
45
|
+
max_rules: 10_000,
|
|
46
|
+
max_any_of_cases: 64,
|
|
47
|
+
max_schema_depth: 64,
|
|
48
|
+
max_path_length: 4_096,
|
|
49
|
+
max_reference_resolution_steps: 64,
|
|
50
|
+
max_selector_expansions: 100_000,
|
|
51
|
+
max_string_length_default: 10_000_000,
|
|
52
|
+
max_container_children_default: 1_000_000,
|
|
53
|
+
};
|
|
54
|
+
function normalizeResourcePolicy(policy, source, ctx) {
|
|
55
|
+
if (policy === undefined)
|
|
56
|
+
return {};
|
|
57
|
+
if (policy === null || typeof policy !== 'object') {
|
|
58
|
+
emitResourceError(ctx, '$', `${source} resource policy must be an object`);
|
|
59
|
+
return {};
|
|
60
|
+
}
|
|
61
|
+
const normalized = {};
|
|
62
|
+
for (const key of Object.keys(policy)) {
|
|
63
|
+
if (!(key in DEFAULT_RESOURCE_POLICY)) {
|
|
64
|
+
emitResourceError(ctx, '$', `Unknown ${source} resource policy key: ${String(key)}`);
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
const value = policy[key];
|
|
68
|
+
if (value !== undefined && (typeof value !== 'number' || !Number.isInteger(value) || value < 0)) {
|
|
69
|
+
emitResourceError(ctx, '$', `${source} resource policy ${String(key)} must be a non-negative integer`);
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
if (value !== undefined) {
|
|
73
|
+
normalized[key] = value;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return normalized;
|
|
77
|
+
}
|
|
78
|
+
function resolveResourcePolicy(schemaPolicy, optionPolicy, ctx) {
|
|
79
|
+
return {
|
|
80
|
+
...DEFAULT_RESOURCE_POLICY,
|
|
81
|
+
...normalizeResourcePolicy(schemaPolicy, 'schema', ctx),
|
|
82
|
+
...normalizeResourcePolicy(optionPolicy, 'option', ctx),
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
function emitResourceError(ctx, path, message, span = null) {
|
|
86
|
+
emitError(ctx, createDiag(path, span, message, ErrorCodes.INVALID_SCHEMA_POLICY));
|
|
87
|
+
}
|
|
88
|
+
const STRING_LIKE_VALUE_TYPES = new Set([
|
|
89
|
+
'StringLiteral',
|
|
90
|
+
'TrimtickLiteral',
|
|
91
|
+
'SeparatorLiteral',
|
|
92
|
+
'HexLiteral',
|
|
93
|
+
'EncodingLiteral',
|
|
94
|
+
'NullLiteral',
|
|
95
|
+
'DateLiteral',
|
|
96
|
+
'TimeLiteral',
|
|
97
|
+
'DateTimeLiteral',
|
|
98
|
+
'ZRUTDateTimeLiteral',
|
|
99
|
+
]);
|
|
100
|
+
function stringLikePayloadLength(event) {
|
|
101
|
+
if (!STRING_LIKE_VALUE_TYPES.has(event.type))
|
|
102
|
+
return null;
|
|
103
|
+
const payload = event.value.length > 0 ? event.value : event.raw;
|
|
104
|
+
return payload.length;
|
|
105
|
+
}
|
|
106
|
+
function enforceStringLengthResourceBudget(info, path, policy, ctx) {
|
|
107
|
+
const payloadLength = stringLikePayloadLength(info);
|
|
108
|
+
if (payloadLength !== null && payloadLength > policy.max_string_length_default) {
|
|
109
|
+
emitResourceError(ctx, path, `String-like payload length ${payloadLength} exceeds max_string_length_default ${policy.max_string_length_default}`, info.span);
|
|
110
|
+
}
|
|
111
|
+
for (const [key, attribute] of info.attributes ?? []) {
|
|
112
|
+
enforceStringLengthResourceBudget(attribute, `${path}@${key}`, policy, ctx);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
function inspectSchemaResourceShape(schema, policy, ctx) {
|
|
116
|
+
for (const rule of schema.rules) {
|
|
117
|
+
const rulePath = typeof rule.path === 'string' && rule.path.length > 0
|
|
118
|
+
? rule.path
|
|
119
|
+
: typeof rule.selector === 'string' && rule.selector.length > 0
|
|
120
|
+
? rule.selector
|
|
121
|
+
: '$';
|
|
122
|
+
if (rulePath.length > policy.max_path_length) {
|
|
123
|
+
emitResourceError(ctx, rulePath, `Rule path length ${rulePath.length} exceeds max_path_length ${policy.max_path_length}`);
|
|
124
|
+
}
|
|
125
|
+
inspectConstraintResourceShape(rule.constraints, rulePath, 1, policy, ctx);
|
|
126
|
+
}
|
|
127
|
+
for (const [datatype, constraints] of Object.entries(schema.datatype_rules ?? {})) {
|
|
128
|
+
inspectConstraintResourceShape(constraints, `datatype_rules.${datatype}`, 1, policy, ctx);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
function inspectConstraintResourceShape(constraints, path, depth, policy, ctx) {
|
|
132
|
+
if (depth > policy.max_schema_depth) {
|
|
133
|
+
emitResourceError(ctx, path, `Schema constraint depth exceeds max_schema_depth ${policy.max_schema_depth}`);
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
if (constraints.any_of !== undefined) {
|
|
137
|
+
if (constraints.any_of.length > policy.max_any_of_cases) {
|
|
138
|
+
emitResourceError(ctx, path, `any_of case count ${constraints.any_of.length} exceeds max_any_of_cases ${policy.max_any_of_cases}`);
|
|
139
|
+
}
|
|
140
|
+
constraints.any_of.forEach((branch, index) => {
|
|
141
|
+
inspectConstraintResourceShape(branch, `${path}.any_of[${index}]`, depth + 1, policy, ctx);
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
if (constraints.attributes !== undefined) {
|
|
145
|
+
for (const [key, child] of Object.entries(constraints.attributes)) {
|
|
146
|
+
inspectConstraintResourceShape(child, `${path}@${key}`, depth + 1, policy, ctx);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
32
150
|
/**
|
|
33
151
|
* Validate an AES against a schema.
|
|
34
152
|
*
|
|
@@ -59,6 +177,20 @@ export function validate(aes, schema, options = {}) {
|
|
|
59
177
|
// TODO: Phase 9 - Guarantees
|
|
60
178
|
// Phase 1: Envelope plumbing
|
|
61
179
|
const ctx = createDiagContext();
|
|
180
|
+
const resourcePolicy = resolveResourcePolicy(schema.resource_policy, options.resourcePolicy, ctx);
|
|
181
|
+
if (ctx.errors.length > 0) {
|
|
182
|
+
return createFailingEnvelope(ctx.errors, ctx.warnings, {});
|
|
183
|
+
}
|
|
184
|
+
if (aes.length > resourcePolicy.max_events) {
|
|
185
|
+
emitResourceError(ctx, '$', `AES event count ${aes.length} exceeds max_events ${resourcePolicy.max_events}`);
|
|
186
|
+
}
|
|
187
|
+
if (schema.rules.length > resourcePolicy.max_rules) {
|
|
188
|
+
emitResourceError(ctx, '$', `Schema rule count ${schema.rules.length} exceeds max_rules ${resourcePolicy.max_rules}`);
|
|
189
|
+
}
|
|
190
|
+
inspectSchemaResourceShape(schema, resourcePolicy, ctx);
|
|
191
|
+
if (ctx.errors.length > 0) {
|
|
192
|
+
return createFailingEnvelope(ctx.errors, ctx.warnings, {});
|
|
193
|
+
}
|
|
62
194
|
// Phase 3: (moved to run after Phase 2)
|
|
63
195
|
// Helpers: format canonical path (local, no runtime AEON deps)
|
|
64
196
|
function formatCanonicalPath(path) {
|
|
@@ -171,6 +303,9 @@ export function validate(aes, schema, options = {}) {
|
|
|
171
303
|
for (let i = 0; i < aes.length; i++) {
|
|
172
304
|
const event = aes[i];
|
|
173
305
|
const pathStr = formatCanonicalPath(event.path);
|
|
306
|
+
if (pathStr.length > resourcePolicy.max_path_length) {
|
|
307
|
+
emitResourceError(ctx, pathStr, `Path length ${pathStr.length} exceeds max_path_length ${resourcePolicy.max_path_length}`, toTuple(event.span));
|
|
308
|
+
}
|
|
174
309
|
if (Array.isArray(event.path?.segments)) {
|
|
175
310
|
for (const seg of event.path.segments) {
|
|
176
311
|
if (seg?.type === 'index') {
|
|
@@ -206,16 +341,31 @@ export function validate(aes, schema, options = {}) {
|
|
|
206
341
|
if ((event.value.type === 'TupleLiteral' || event.value.type === 'ListLiteral' || event.value.type === 'ListNode')
|
|
207
342
|
&& Array.isArray(event.value.elements)) {
|
|
208
343
|
containerArity.set(pathStr, event.value.elements.length);
|
|
344
|
+
if (event.value.elements.length > resourcePolicy.max_container_children_default) {
|
|
345
|
+
emitResourceError(ctx, pathStr, `Container child count ${event.value.elements.length} exceeds max_container_children_default ${resourcePolicy.max_container_children_default}`, toTuple(event.span));
|
|
346
|
+
}
|
|
209
347
|
hydrateIndexedFallback(pathStr, event.value, toTuple(event.span));
|
|
210
348
|
}
|
|
349
|
+
else if (event.value.type === 'ObjectNode' && Array.isArray(event.value.bindings)) {
|
|
350
|
+
containerArity.set(pathStr, event.value.bindings.length);
|
|
351
|
+
if (event.value.bindings.length > resourcePolicy.max_container_children_default) {
|
|
352
|
+
emitResourceError(ctx, pathStr, `Container child count ${event.value.bindings.length} exceeds max_container_children_default ${resourcePolicy.max_container_children_default}`, toTuple(event.span));
|
|
353
|
+
}
|
|
354
|
+
}
|
|
211
355
|
else if (event.value.type === 'NodeLiteral' && Array.isArray(event.value.children)) {
|
|
212
356
|
containerArity.set(pathStr, event.value.children.length);
|
|
357
|
+
if (event.value.children.length > resourcePolicy.max_container_children_default) {
|
|
358
|
+
emitResourceError(ctx, pathStr, `Container child count ${event.value.children.length} exceeds max_container_children_default ${resourcePolicy.max_container_children_default}`, toTuple(event.span));
|
|
359
|
+
}
|
|
213
360
|
hydrateIndexedFallback(pathStr, event.value, toTuple(event.span));
|
|
214
361
|
}
|
|
215
362
|
}
|
|
216
363
|
}
|
|
217
364
|
// Register index even for first occurrence
|
|
218
365
|
}
|
|
366
|
+
for (const [path, info] of eventsByPath) {
|
|
367
|
+
enforceStringLengthResourceBudget(info, path, resourcePolicy, ctx);
|
|
368
|
+
}
|
|
219
369
|
// Optional separator literal trailing-delimiter policy
|
|
220
370
|
if (trailingSeparatorPolicy !== 'off') {
|
|
221
371
|
for (const event of aes) {
|
|
@@ -240,33 +390,63 @@ export function validate(aes, schema, options = {}) {
|
|
|
240
390
|
}
|
|
241
391
|
// Phase 3: Build rule index from schema (run after baseline invariants)
|
|
242
392
|
const ruleIndex = buildRuleIndex(schema, ctx);
|
|
393
|
+
const selectorExpansionBudget = { count: 0 };
|
|
394
|
+
const expandedRuleIndex = expandWildcardRules(expandSelectorRules(ruleIndex, schema, eventsByPath, ctx, resourcePolicy, selectorExpansionBudget), eventsByPath, ctx, resourcePolicy, selectorExpansionBudget);
|
|
395
|
+
const effectiveRuleIndex = mergeDatatypeRules(expandedRuleIndex, schema.datatype_rules, eventsByPath);
|
|
243
396
|
// Phase 4: Presence checks (required fields)
|
|
244
397
|
const boundPaths = new Set(seen.keys());
|
|
245
|
-
checkPresence(
|
|
398
|
+
checkPresence(effectiveRuleIndex, boundPaths, ctx);
|
|
246
399
|
checkWorldPolicy(schema, aes, boundPaths, ctx);
|
|
247
400
|
// Phase 5: Type checks (literal kind)
|
|
248
|
-
checkReferenceForms(schema,
|
|
249
|
-
const effectiveEventsByPath = resolveReferenceFormEvents(
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
401
|
+
checkReferenceForms(schema, effectiveRuleIndex, eventsByPath, ctx);
|
|
402
|
+
const effectiveEventsByPath = resolveReferenceFormEvents(effectiveRuleIndex, eventsByPath, resourcePolicy, ctx);
|
|
403
|
+
const selectedRuleIndex = selectAnyOfRules(effectiveRuleIndex, effectiveEventsByPath, ctx);
|
|
404
|
+
checkTypes(selectedRuleIndex, effectiveEventsByPath, ctx);
|
|
405
|
+
// Phase 5b: core v1 arity/cardinality checks for tuple/list/node containers
|
|
406
|
+
for (const [path, rule] of selectedRuleIndex) {
|
|
407
|
+
const { length_exact, min_children, max_children } = rule.constraints;
|
|
408
|
+
if (length_exact === undefined && min_children === undefined && max_children === undefined)
|
|
255
409
|
continue;
|
|
256
410
|
const actualLength = containerArity.get(path);
|
|
257
411
|
if (actualLength === undefined)
|
|
258
412
|
continue;
|
|
259
|
-
|
|
413
|
+
const span = eventsByPath.get(path)?.span ?? null;
|
|
414
|
+
if (typeof length_exact === 'number' && actualLength !== length_exact) {
|
|
415
|
+
emitError(ctx, createDiag(path, span, `Container cardinality mismatch: expected exactly ${length_exact} children, got ${actualLength}`, ErrorCodes.TUPLE_ARITY_MISMATCH));
|
|
416
|
+
}
|
|
417
|
+
if (typeof min_children === 'number' && actualLength < min_children) {
|
|
418
|
+
emitError(ctx, createDiag(path, span, `Container cardinality mismatch: expected at least ${min_children} children, got ${actualLength}`, ErrorCodes.CONTAINER_CARDINALITY_MISMATCH));
|
|
419
|
+
}
|
|
420
|
+
if (typeof max_children === 'number' && actualLength > max_children) {
|
|
421
|
+
emitError(ctx, createDiag(path, span, `Container cardinality mismatch: expected at most ${max_children} children, got ${actualLength}`, ErrorCodes.CONTAINER_CARDINALITY_MISMATCH));
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
checkLexicalLiteralConstraints(selectedRuleIndex, effectiveEventsByPath, ctx);
|
|
425
|
+
// Phase 5c: constraints that widen NumberLiteral type acceptance to infinity/NaN
|
|
426
|
+
for (const [path, rule] of selectedRuleIndex) {
|
|
427
|
+
const event = effectiveEventsByPath.get(path);
|
|
428
|
+
if (!event)
|
|
429
|
+
continue;
|
|
430
|
+
if (event.type === 'InfinityLiteral' && rule.constraints.allow_infinity !== true) {
|
|
431
|
+
continue;
|
|
432
|
+
}
|
|
433
|
+
if (event.type === 'NaNLiteral' && rule.constraints.allow_nan !== true) {
|
|
434
|
+
continue;
|
|
435
|
+
}
|
|
436
|
+
if ((event.type === 'InfinityLiteral' || event.type === 'NaNLiteral')
|
|
437
|
+
&& rule.constraints.type !== undefined
|
|
438
|
+
&& !isNumericExpectedType(rule.constraints.type)) {
|
|
260
439
|
const span = eventsByPath.get(path)?.span ?? null;
|
|
261
|
-
emitError(ctx, createDiag(path, span, `
|
|
440
|
+
emitError(ctx, createDiag(path, span, `Type mismatch: expected ${rule.constraints.type}, got ${event.type}`, ErrorCodes.TYPE_MISMATCH));
|
|
262
441
|
}
|
|
263
442
|
}
|
|
264
443
|
// Phase 6: Numeric form constraints (sign, digit count)
|
|
265
|
-
checkNumericForm(
|
|
444
|
+
checkNumericForm(selectedRuleIndex, effectiveEventsByPath, ctx);
|
|
266
445
|
// Phase 7: String form constraints (length, pattern)
|
|
267
|
-
checkStringForm(
|
|
268
|
-
checkPatterns(
|
|
269
|
-
|
|
446
|
+
checkStringForm(selectedRuleIndex, effectiveEventsByPath, ctx);
|
|
447
|
+
checkPatterns(selectedRuleIndex, effectiveEventsByPath, ctx);
|
|
448
|
+
checkAttributePolicy(schema, selectedRuleIndex, effectiveEventsByPath, ctx);
|
|
449
|
+
checkAttributeConstraints(selectedRuleIndex, effectiveEventsByPath, schema.datatype_rules, ctx);
|
|
270
450
|
checkDatatypeRules(schema.datatype_rules, effectiveEventsByPath, ctx);
|
|
271
451
|
if (ctx.errors.length > 0) {
|
|
272
452
|
return createFailingEnvelope(ctx.errors, ctx.warnings, {});
|
|
@@ -317,7 +497,13 @@ export function validate(aes, schema, options = {}) {
|
|
|
317
497
|
function checkWorldPolicy(schema, aes, boundPaths, ctx) {
|
|
318
498
|
if ((schema.world ?? 'open') !== 'closed')
|
|
319
499
|
return;
|
|
320
|
-
const
|
|
500
|
+
const allowedRules = schema.rules
|
|
501
|
+
.map((rule) => typeof rule.path === 'string' && rule.path.length > 0
|
|
502
|
+
? { kind: 'path', value: rule.path }
|
|
503
|
+
: typeof rule.selector === 'string' && rule.selector.length > 0
|
|
504
|
+
? { kind: 'selector', value: rule.selector }
|
|
505
|
+
: null)
|
|
506
|
+
.filter((rule) => rule !== null);
|
|
321
507
|
for (const event of aes) {
|
|
322
508
|
const key = typeof event.key === 'string' ? event.key : '';
|
|
323
509
|
if (key.startsWith('aeon:'))
|
|
@@ -325,12 +511,14 @@ function checkWorldPolicy(schema, aes, boundPaths, ctx) {
|
|
|
325
511
|
const path = formatCanonicalPathLocal(event.path);
|
|
326
512
|
if (!boundPaths.has(path))
|
|
327
513
|
continue;
|
|
328
|
-
if (
|
|
514
|
+
if (allowedRules.some((rule) => rule.kind === 'selector'
|
|
515
|
+
? matchesSelectorPath(path, rule.value)
|
|
516
|
+
: matchesAllowedPath(path, rule.value)))
|
|
329
517
|
continue;
|
|
330
518
|
emitError(ctx, createDiag(path, toTupleLocal(event.span), `Binding '${path}' is not allowed by closed-world schema`, ErrorCodes.UNEXPECTED_BINDING));
|
|
331
519
|
}
|
|
332
520
|
}
|
|
333
|
-
function resolveReferenceFormEvents(ruleIndex, eventsByPath) {
|
|
521
|
+
function resolveReferenceFormEvents(ruleIndex, eventsByPath, resourcePolicy, ctx) {
|
|
334
522
|
const resolved = new Map(eventsByPath);
|
|
335
523
|
for (const [path, rule] of ruleIndex.entries()) {
|
|
336
524
|
if (rule.constraints.resolve_reference_form !== true)
|
|
@@ -338,8 +526,12 @@ function resolveReferenceFormEvents(ruleIndex, eventsByPath) {
|
|
|
338
526
|
const event = eventsByPath.get(path);
|
|
339
527
|
if (!event || !isReferenceType(event.type) || !event.referencePath)
|
|
340
528
|
continue;
|
|
341
|
-
const
|
|
529
|
+
const resolutionState = { exhausted: false };
|
|
530
|
+
const terminal = resolveTerminalReferenceEvent(event, eventsByPath, new Set(), resourcePolicy.max_reference_resolution_steps, resolutionState);
|
|
342
531
|
if (!terminal) {
|
|
532
|
+
if (resolutionState.exhausted) {
|
|
533
|
+
emitResourceError(ctx, path, `Reference resolution exceeded max_reference_resolution_steps ${resourcePolicy.max_reference_resolution_steps}`, event.span);
|
|
534
|
+
}
|
|
343
535
|
resolved.delete(path);
|
|
344
536
|
continue;
|
|
345
537
|
}
|
|
@@ -350,10 +542,135 @@ function resolveReferenceFormEvents(ruleIndex, eventsByPath) {
|
|
|
350
542
|
}
|
|
351
543
|
return resolved;
|
|
352
544
|
}
|
|
353
|
-
function
|
|
545
|
+
function expandSelectorRules(ruleIndex, schema, eventsByPath, ctx, resourcePolicy, expansionBudget) {
|
|
546
|
+
const expanded = new Map(ruleIndex);
|
|
547
|
+
for (const rule of schema.rules) {
|
|
548
|
+
if (typeof rule.selector !== 'string' || rule.selector.length === 0)
|
|
549
|
+
continue;
|
|
550
|
+
if (typeof rule.path === 'string' && rule.path.length > 0)
|
|
551
|
+
continue;
|
|
552
|
+
let matched = false;
|
|
553
|
+
for (const actualPath of eventsByPath.keys()) {
|
|
554
|
+
if (!matchesSelectorPath(actualPath, rule.selector))
|
|
555
|
+
continue;
|
|
556
|
+
matched = true;
|
|
557
|
+
expansionBudget.count += 1;
|
|
558
|
+
if (expansionBudget.count > resourcePolicy.max_selector_expansions) {
|
|
559
|
+
emitResourceError(ctx, rule.selector, `Selector expansion count exceeds max_selector_expansions ${resourcePolicy.max_selector_expansions}`);
|
|
560
|
+
return expanded;
|
|
561
|
+
}
|
|
562
|
+
if (!expanded.has(actualPath)) {
|
|
563
|
+
expanded.set(actualPath, { ...rule, path: actualPath });
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
if (!matched && rule.constraints.required === true) {
|
|
567
|
+
emitError(ctx, createDiag(rule.selector, null, `Missing required field: ${rule.selector}`, ErrorCodes.MISSING_REQUIRED_FIELD));
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
return expanded;
|
|
571
|
+
}
|
|
572
|
+
function expandWildcardRules(ruleIndex, eventsByPath, ctx, resourcePolicy, expansionBudget) {
|
|
573
|
+
const expanded = new Map(ruleIndex);
|
|
574
|
+
for (const [path, rule] of ruleIndex.entries()) {
|
|
575
|
+
if (!path.includes('[*]'))
|
|
576
|
+
continue;
|
|
577
|
+
expanded.delete(path);
|
|
578
|
+
for (const actualPath of eventsByPath.keys()) {
|
|
579
|
+
if (matchesAllowedPath(actualPath, path)) {
|
|
580
|
+
expansionBudget.count += 1;
|
|
581
|
+
if (expansionBudget.count > resourcePolicy.max_selector_expansions) {
|
|
582
|
+
emitResourceError(ctx, path, `Wildcard expansion count exceeds max_selector_expansions ${resourcePolicy.max_selector_expansions}`);
|
|
583
|
+
return expanded;
|
|
584
|
+
}
|
|
585
|
+
expanded.set(actualPath, { ...rule, path: actualPath });
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
return expanded;
|
|
590
|
+
}
|
|
591
|
+
function selectAnyOfRules(ruleIndex, eventsByPath, ctx) {
|
|
592
|
+
const selected = new Map(ruleIndex);
|
|
593
|
+
for (const [path, rule] of ruleIndex.entries()) {
|
|
594
|
+
if (!Array.isArray(rule.constraints.any_of))
|
|
595
|
+
continue;
|
|
596
|
+
const event = eventsByPath.get(path);
|
|
597
|
+
if (!event)
|
|
598
|
+
continue;
|
|
599
|
+
const outer = withoutAnyOf(rule.constraints);
|
|
600
|
+
const branch = rule.constraints.any_of.find((candidate) => constraintBranchMatchesEvent(candidate, event));
|
|
601
|
+
if (!branch) {
|
|
602
|
+
emitError(ctx, createDiag(path, event.span, `Value does not match any allowed constraint branch at ${path}`, ErrorCodes.TYPE_MISMATCH));
|
|
603
|
+
selected.set(path, { ...rule, constraints: outer });
|
|
604
|
+
continue;
|
|
605
|
+
}
|
|
606
|
+
selected.set(path, { ...rule, constraints: { ...outer, ...branch } });
|
|
607
|
+
}
|
|
608
|
+
return selected;
|
|
609
|
+
}
|
|
610
|
+
function withoutAnyOf(constraints) {
|
|
611
|
+
const { any_of: _anyOf, ...rest } = constraints;
|
|
612
|
+
return rest;
|
|
613
|
+
}
|
|
614
|
+
function patternMatches(pattern, value) {
|
|
615
|
+
return matchesPortablePattern(pattern, value);
|
|
616
|
+
}
|
|
617
|
+
function constraintBranchMatchesEvent(constraints, event) {
|
|
618
|
+
if (constraints.type_is !== undefined) {
|
|
619
|
+
const containerOk = constraints.type_is === 'list'
|
|
620
|
+
? (event.type === 'ListLiteral' || event.type === 'ListNode')
|
|
621
|
+
: event.type === 'TupleLiteral';
|
|
622
|
+
if (!containerOk)
|
|
623
|
+
return false;
|
|
624
|
+
}
|
|
625
|
+
if (constraints.type !== undefined && !constraintTypeMatches(event.type, constraints.type, event.raw, constraints)) {
|
|
626
|
+
return false;
|
|
627
|
+
}
|
|
628
|
+
if (constraints.datatype !== undefined && event.datatype !== constraints.datatype) {
|
|
629
|
+
return false;
|
|
630
|
+
}
|
|
631
|
+
if (event.type === 'NullLiteral' && !nullValueMatches(event.value, constraints)) {
|
|
632
|
+
return false;
|
|
633
|
+
}
|
|
634
|
+
if (event.type === 'ToggleLiteral' && constraints.toggle_pair !== undefined && constraints.toggle_pair !== 'any') {
|
|
635
|
+
const value = (event.raw || event.value).toLowerCase();
|
|
636
|
+
const allowed = constraints.toggle_pair === 'yes_no'
|
|
637
|
+
? ['yes', 'no']
|
|
638
|
+
: constraints.toggle_pair === 'on_off'
|
|
639
|
+
? ['on', 'off']
|
|
640
|
+
: [];
|
|
641
|
+
if (allowed.length > 0 && !allowed.includes(value))
|
|
642
|
+
return false;
|
|
643
|
+
}
|
|
644
|
+
if (isStringType(event.type)) {
|
|
645
|
+
const valueLength = event.value.length;
|
|
646
|
+
if (constraints.min_length !== undefined && valueLength < constraints.min_length)
|
|
647
|
+
return false;
|
|
648
|
+
if (constraints.max_length !== undefined && valueLength > constraints.max_length)
|
|
649
|
+
return false;
|
|
650
|
+
if (!patternMatches(constraints.pattern, event.value))
|
|
651
|
+
return false;
|
|
652
|
+
}
|
|
653
|
+
if (hasDigitFormConstraints(constraints) && isDigitFormLiteral(event.type)) {
|
|
654
|
+
const digitCount = countFormDigits(event.type, event.raw);
|
|
655
|
+
if (constraints.sign === 'unsigned' && isFormNegative(event.raw))
|
|
656
|
+
return false;
|
|
657
|
+
if (constraints.min_digits !== undefined && digitCount < constraints.min_digits)
|
|
658
|
+
return false;
|
|
659
|
+
if (constraints.max_digits !== undefined && digitCount > constraints.max_digits)
|
|
660
|
+
return false;
|
|
661
|
+
if (event.type === 'RadixLiteral' && constraints.radix !== undefined && !radixConstraintMatches(event.datatype, event.raw, constraints))
|
|
662
|
+
return false;
|
|
663
|
+
}
|
|
664
|
+
return true;
|
|
665
|
+
}
|
|
666
|
+
function resolveTerminalReferenceEvent(event, eventsByPath, activePaths, remainingSteps, state) {
|
|
354
667
|
if (!isReferenceType(event.type) || !event.referencePath) {
|
|
355
668
|
return event;
|
|
356
669
|
}
|
|
670
|
+
if (remainingSteps <= 0) {
|
|
671
|
+
state.exhausted = true;
|
|
672
|
+
return null;
|
|
673
|
+
}
|
|
357
674
|
const targetPath = formatReferenceLookupPath(event.referencePath);
|
|
358
675
|
if (activePaths.has(targetPath)) {
|
|
359
676
|
return null;
|
|
@@ -364,7 +681,7 @@ function resolveTerminalReferenceEvent(event, eventsByPath, activePaths) {
|
|
|
364
681
|
}
|
|
365
682
|
activePaths.add(targetPath);
|
|
366
683
|
const resolved = isReferenceType(target.type)
|
|
367
|
-
? resolveTerminalReferenceEvent(target, eventsByPath, activePaths)
|
|
684
|
+
? resolveTerminalReferenceEvent(target, eventsByPath, activePaths, remainingSteps - 1, state)
|
|
368
685
|
: target;
|
|
369
686
|
activePaths.delete(targetPath);
|
|
370
687
|
return resolved;
|
|
@@ -404,6 +721,179 @@ function matchesAllowedPath(actualPath, allowedPath) {
|
|
|
404
721
|
const pattern = `^${escaped}$`;
|
|
405
722
|
return new RegExp(pattern).test(actualPath);
|
|
406
723
|
}
|
|
724
|
+
function tokenizeCanonicalLikePath(path) {
|
|
725
|
+
if (!path.startsWith('$'))
|
|
726
|
+
return null;
|
|
727
|
+
const segments = [];
|
|
728
|
+
let index = 1;
|
|
729
|
+
while (index < path.length) {
|
|
730
|
+
const marker = path[index];
|
|
731
|
+
if (marker === '.') {
|
|
732
|
+
index += 1;
|
|
733
|
+
if (path[index] === '[') {
|
|
734
|
+
const end = findBracketEnd(path, index);
|
|
735
|
+
if (end < 0)
|
|
736
|
+
return null;
|
|
737
|
+
segments.push(path.slice(index, end + 1));
|
|
738
|
+
index = end + 1;
|
|
739
|
+
continue;
|
|
740
|
+
}
|
|
741
|
+
const start = index;
|
|
742
|
+
while (index < path.length && !['.', '[', '@'].includes(path[index])) {
|
|
743
|
+
index += 1;
|
|
744
|
+
}
|
|
745
|
+
if (start === index)
|
|
746
|
+
return null;
|
|
747
|
+
segments.push(path.slice(start, index));
|
|
748
|
+
continue;
|
|
749
|
+
}
|
|
750
|
+
if (marker === '[') {
|
|
751
|
+
const end = findBracketEnd(path, index);
|
|
752
|
+
if (end < 0)
|
|
753
|
+
return null;
|
|
754
|
+
segments.push(path.slice(index, end + 1));
|
|
755
|
+
index = end + 1;
|
|
756
|
+
continue;
|
|
757
|
+
}
|
|
758
|
+
if (marker === '@') {
|
|
759
|
+
index += 1;
|
|
760
|
+
if (path[index] === '[') {
|
|
761
|
+
const end = findBracketEnd(path, index);
|
|
762
|
+
if (end < 0)
|
|
763
|
+
return null;
|
|
764
|
+
segments.push(`@${path.slice(index, end + 1)}`);
|
|
765
|
+
index = end + 1;
|
|
766
|
+
continue;
|
|
767
|
+
}
|
|
768
|
+
const start = index;
|
|
769
|
+
while (index < path.length && !['.', '[', '@'].includes(path[index])) {
|
|
770
|
+
index += 1;
|
|
771
|
+
}
|
|
772
|
+
if (start === index)
|
|
773
|
+
return null;
|
|
774
|
+
segments.push(`@${path.slice(start, index)}`);
|
|
775
|
+
continue;
|
|
776
|
+
}
|
|
777
|
+
return null;
|
|
778
|
+
}
|
|
779
|
+
return segments;
|
|
780
|
+
}
|
|
781
|
+
function findBracketEnd(path, start) {
|
|
782
|
+
let quote = null;
|
|
783
|
+
let escaped = false;
|
|
784
|
+
for (let index = start + 1; index < path.length; index++) {
|
|
785
|
+
const ch = path[index];
|
|
786
|
+
if (escaped) {
|
|
787
|
+
escaped = false;
|
|
788
|
+
continue;
|
|
789
|
+
}
|
|
790
|
+
if (quote) {
|
|
791
|
+
if (ch === '\\') {
|
|
792
|
+
escaped = true;
|
|
793
|
+
}
|
|
794
|
+
else if (ch === quote) {
|
|
795
|
+
quote = null;
|
|
796
|
+
}
|
|
797
|
+
continue;
|
|
798
|
+
}
|
|
799
|
+
if (ch === '"' || ch === "'") {
|
|
800
|
+
quote = ch;
|
|
801
|
+
continue;
|
|
802
|
+
}
|
|
803
|
+
if (ch === ']')
|
|
804
|
+
return index;
|
|
805
|
+
}
|
|
806
|
+
return -1;
|
|
807
|
+
}
|
|
808
|
+
function matchesSelectorPath(actualPath, selector) {
|
|
809
|
+
if (actualPath === selector)
|
|
810
|
+
return true;
|
|
811
|
+
const actualSegments = tokenizeCanonicalLikePath(actualPath);
|
|
812
|
+
const selectorSegments = tokenizeCanonicalLikePath(selector);
|
|
813
|
+
if (!actualSegments || !selectorSegments)
|
|
814
|
+
return false;
|
|
815
|
+
const matchFrom = (actualIndex, selectorIndex) => {
|
|
816
|
+
if (selectorIndex === selectorSegments.length) {
|
|
817
|
+
return actualIndex === actualSegments.length;
|
|
818
|
+
}
|
|
819
|
+
const selectorSegment = selectorSegments[selectorIndex];
|
|
820
|
+
if (selectorSegment === '**') {
|
|
821
|
+
if (selectorIndex === selectorSegments.length - 1)
|
|
822
|
+
return true;
|
|
823
|
+
for (let nextActual = actualIndex; nextActual <= actualSegments.length; nextActual++) {
|
|
824
|
+
if (matchFrom(nextActual, selectorIndex + 1))
|
|
825
|
+
return true;
|
|
826
|
+
}
|
|
827
|
+
return false;
|
|
828
|
+
}
|
|
829
|
+
if (actualIndex >= actualSegments.length)
|
|
830
|
+
return false;
|
|
831
|
+
if (selectorSegment === '*') {
|
|
832
|
+
return matchFrom(actualIndex + 1, selectorIndex + 1);
|
|
833
|
+
}
|
|
834
|
+
if (selectorSegment === '[*]') {
|
|
835
|
+
return /^\[\d+\]$/.test(actualSegments[actualIndex])
|
|
836
|
+
&& matchFrom(actualIndex + 1, selectorIndex + 1);
|
|
837
|
+
}
|
|
838
|
+
return selectorSegment === actualSegments[actualIndex]
|
|
839
|
+
&& matchFrom(actualIndex + 1, selectorIndex + 1);
|
|
840
|
+
};
|
|
841
|
+
return matchFrom(0, 0);
|
|
842
|
+
}
|
|
843
|
+
function collectAllowedAttributePaths(ruleIndex) {
|
|
844
|
+
const allowed = [];
|
|
845
|
+
function visit(basePath, constraints) {
|
|
846
|
+
if (basePath.includes('@')) {
|
|
847
|
+
allowed.push(basePath);
|
|
848
|
+
}
|
|
849
|
+
const attributes = constraints.attributes;
|
|
850
|
+
if (!attributes)
|
|
851
|
+
return;
|
|
852
|
+
for (const [key, childConstraints] of Object.entries(attributes)) {
|
|
853
|
+
visit(`${basePath}@${key}`, childConstraints);
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
for (const [path, rule] of ruleIndex) {
|
|
857
|
+
visit(path, rule.constraints);
|
|
858
|
+
}
|
|
859
|
+
return allowed;
|
|
860
|
+
}
|
|
861
|
+
function collectAttributeEntries(eventsByPath) {
|
|
862
|
+
const entries = [];
|
|
863
|
+
function visit(basePath, attributes) {
|
|
864
|
+
if (!attributes)
|
|
865
|
+
return;
|
|
866
|
+
for (const [key, entry] of attributes.entries()) {
|
|
867
|
+
const path = `${basePath}@${key}`;
|
|
868
|
+
entries.push({ path, span: entry.span });
|
|
869
|
+
visit(path, entry.attributes);
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
for (const [path, event] of eventsByPath) {
|
|
873
|
+
visit(path, event.attributes);
|
|
874
|
+
}
|
|
875
|
+
return entries;
|
|
876
|
+
}
|
|
877
|
+
function checkAttributePolicy(schema, ruleIndex, eventsByPath, ctx) {
|
|
878
|
+
const policy = schema.attribute_policy ?? 'inherit_world';
|
|
879
|
+
if (policy === 'inherit_world' && (schema.world ?? 'open') !== 'closed')
|
|
880
|
+
return;
|
|
881
|
+
if (policy !== 'inherit_world' && policy !== 'forbid')
|
|
882
|
+
return;
|
|
883
|
+
const attributeEntries = collectAttributeEntries(eventsByPath);
|
|
884
|
+
if (attributeEntries.length === 0)
|
|
885
|
+
return;
|
|
886
|
+
const allowedPaths = policy === 'inherit_world'
|
|
887
|
+
? collectAllowedAttributePaths(ruleIndex)
|
|
888
|
+
: [];
|
|
889
|
+
for (const entry of attributeEntries) {
|
|
890
|
+
if (allowedPaths.some((allowedPath) => matchesAllowedPath(entry.path, allowedPath)))
|
|
891
|
+
continue;
|
|
892
|
+
emitError(ctx, createDiag(entry.path, entry.span, policy === 'forbid'
|
|
893
|
+
? `Attribute '${entry.path}' is forbidden by schema attribute_policy`
|
|
894
|
+
: `Attribute '${entry.path}' is not allowed by closed-world schema`, ErrorCodes.UNEXPECTED_ATTRIBUTE_ENTRY));
|
|
895
|
+
}
|
|
896
|
+
}
|
|
407
897
|
function checkDatatypeRules(datatypeRules, eventsByPath, ctx) {
|
|
408
898
|
if (!datatypeRules)
|
|
409
899
|
return;
|
|
@@ -434,22 +924,43 @@ function checkDatatypeRules(datatypeRules, eventsByPath, ctx) {
|
|
|
434
924
|
continue;
|
|
435
925
|
}
|
|
436
926
|
if (constraints.min_value !== undefined || constraints.max_value !== undefined) {
|
|
437
|
-
const
|
|
438
|
-
if (!
|
|
439
|
-
emitError(ctx, createDiag(path, event.span, `Datatype rule violation for ':${event.datatype}':
|
|
927
|
+
const range = normalizeRangeLiteral(event.type, raw);
|
|
928
|
+
if (!range) {
|
|
929
|
+
emitError(ctx, createDiag(path, event.span, `Datatype rule violation for ':${event.datatype}': range constraints require numeric literal form`, ErrorCodes.NUMERIC_FORM_VIOLATION));
|
|
440
930
|
continue;
|
|
441
931
|
}
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
emitError(ctx, createDiag(path, event.span, `Datatype rule violation for ':${event.datatype}': expected value >= ${constraints.min_value}, got ${normalized}`, ErrorCodes.NUMERIC_FORM_VIOLATION));
|
|
932
|
+
if (constraints.min_value !== undefined && isBelowRange(range, constraints.min_value)) {
|
|
933
|
+
emitError(ctx, createDiag(path, event.span, `Datatype rule violation for ':${event.datatype}': expected value >= ${constraints.min_value}, got ${range.raw}`, ErrorCodes.NUMERIC_FORM_VIOLATION));
|
|
445
934
|
continue;
|
|
446
935
|
}
|
|
447
|
-
if (constraints.max_value !== undefined &&
|
|
448
|
-
emitError(ctx, createDiag(path, event.span, `Datatype rule violation for ':${event.datatype}': expected value <= ${constraints.max_value}, got ${
|
|
936
|
+
if (constraints.max_value !== undefined && isAboveRange(range, constraints.max_value)) {
|
|
937
|
+
emitError(ctx, createDiag(path, event.span, `Datatype rule violation for ':${event.datatype}': expected value <= ${constraints.max_value}, got ${range.raw}`, ErrorCodes.NUMERIC_FORM_VIOLATION));
|
|
449
938
|
}
|
|
450
939
|
}
|
|
451
940
|
}
|
|
452
941
|
}
|
|
942
|
+
function mergeDatatypeRules(ruleIndex, datatypeRules, eventsByPath) {
|
|
943
|
+
if (!datatypeRules)
|
|
944
|
+
return ruleIndex;
|
|
945
|
+
const merged = new Map();
|
|
946
|
+
for (const [path, rule] of ruleIndex.entries()) {
|
|
947
|
+
merged.set(path, { ...rule, constraints: { ...rule.constraints } });
|
|
948
|
+
}
|
|
949
|
+
for (const [path, event] of eventsByPath.entries()) {
|
|
950
|
+
if (!event.datatype)
|
|
951
|
+
continue;
|
|
952
|
+
const datatypeRule = datatypeRules[datatypeBase(event.datatype).toLowerCase()];
|
|
953
|
+
if (!datatypeRule)
|
|
954
|
+
continue;
|
|
955
|
+
const existing = merged.get(path);
|
|
956
|
+
const constraints = existing?.constraints ?? {};
|
|
957
|
+
merged.set(path, {
|
|
958
|
+
path,
|
|
959
|
+
constraints: { ...datatypeRule, ...constraints },
|
|
960
|
+
});
|
|
961
|
+
}
|
|
962
|
+
return merged;
|
|
963
|
+
}
|
|
453
964
|
function checkAttributeConstraints(ruleIndex, eventsByPath, datatypeRules, ctx) {
|
|
454
965
|
for (const [path, rule] of ruleIndex) {
|
|
455
966
|
if (!rule.constraints.attributes && rule.constraints.closed_attributes !== true)
|
|
@@ -493,9 +1004,10 @@ function validateAttributeEntry(path, entry, constraints, datatypeRules, ctx) {
|
|
|
493
1004
|
emitError(ctx, createDiag(path, entry.span, `Container kind mismatch: expected ${effectiveConstraints.type_is}, got ${entry.type}`, ErrorCodes.WRONG_CONTAINER_KIND));
|
|
494
1005
|
}
|
|
495
1006
|
}
|
|
496
|
-
if (effectiveConstraints.type !== undefined && !constraintTypeMatches(entry.type, effectiveConstraints.type, entry.raw)) {
|
|
1007
|
+
if (effectiveConstraints.type !== undefined && !constraintTypeMatches(entry.type, effectiveConstraints.type, entry.raw, effectiveConstraints)) {
|
|
497
1008
|
emitError(ctx, createDiag(path, entry.span, `Type mismatch: expected ${effectiveConstraints.type}, got ${entry.type}`, ErrorCodes.TYPE_MISMATCH));
|
|
498
1009
|
}
|
|
1010
|
+
checkLexicalLiteralConstraint(path, entry, effectiveConstraints, ctx);
|
|
499
1011
|
if (effectiveConstraints.datatype !== undefined && entry.datatype !== effectiveConstraints.datatype) {
|
|
500
1012
|
emitError(ctx, createDiag(path, entry.span, `Datatype mismatch: expected ${effectiveConstraints.datatype}, got ${entry.datatype ?? '<none>'}`, ErrorCodes.TYPE_MISMATCH));
|
|
501
1013
|
}
|
|
@@ -511,9 +1023,9 @@ function validateAttributeEntry(path, entry, constraints, datatypeRules, ctx) {
|
|
|
511
1023
|
emitError(ctx, createDiag(path, entry.span, `Reference kind mismatch at ${path}: expected ${expectedType}, got ${entry.type}`, ErrorCodes.REFERENCE_KIND_MISMATCH));
|
|
512
1024
|
}
|
|
513
1025
|
}
|
|
514
|
-
if (entry.type
|
|
515
|
-
const digitCount =
|
|
516
|
-
if (effectiveConstraints.sign === 'unsigned' &&
|
|
1026
|
+
if (hasDigitFormConstraints(effectiveConstraints) && isDigitFormLiteral(entry.type)) {
|
|
1027
|
+
const digitCount = countFormDigits(entry.type, entry.raw);
|
|
1028
|
+
if ((entry.type === 'NumberLiteral' || entry.type === 'RadixLiteral') && effectiveConstraints.sign === 'unsigned' && isFormNegative(entry.raw)) {
|
|
517
1029
|
emitError(ctx, createDiag(path, entry.span, `Numeric form violation: expected unsigned, got negative`, ErrorCodes.NUMERIC_FORM_VIOLATION));
|
|
518
1030
|
}
|
|
519
1031
|
if (effectiveConstraints.min_digits !== undefined && digitCount < effectiveConstraints.min_digits) {
|
|
@@ -522,15 +1034,27 @@ function validateAttributeEntry(path, entry, constraints, datatypeRules, ctx) {
|
|
|
522
1034
|
if (effectiveConstraints.max_digits !== undefined && digitCount > effectiveConstraints.max_digits) {
|
|
523
1035
|
emitError(ctx, createDiag(path, entry.span, `Numeric form violation: expected max ${effectiveConstraints.max_digits} digits, got ${digitCount}`, ErrorCodes.NUMERIC_FORM_VIOLATION));
|
|
524
1036
|
}
|
|
1037
|
+
if (entry.type === 'RadixLiteral' && effectiveConstraints.radix !== undefined) {
|
|
1038
|
+
const declaredRadix = declaredRadixFromDatatype(entry.datatype);
|
|
1039
|
+
if ((declaredRadix === null && effectiveConstraints.allow_unspecified_radix !== true)
|
|
1040
|
+
|| (declaredRadix !== null && declaredRadix !== effectiveConstraints.radix)) {
|
|
1041
|
+
emitError(ctx, createDiag(path, entry.span, `Numeric form violation: expected declared radix ${effectiveConstraints.radix}`, ErrorCodes.NUMERIC_FORM_VIOLATION));
|
|
1042
|
+
return;
|
|
1043
|
+
}
|
|
1044
|
+
const invalidDigit = firstInvalidRadixDigit(entry.raw, effectiveConstraints.radix);
|
|
1045
|
+
if (invalidDigit !== null) {
|
|
1046
|
+
emitError(ctx, createDiag(path, entry.span, `Numeric form violation: radix literal digit '${invalidDigit}' is outside radix ${effectiveConstraints.radix}`, ErrorCodes.NUMERIC_FORM_VIOLATION));
|
|
1047
|
+
}
|
|
1048
|
+
}
|
|
525
1049
|
}
|
|
526
|
-
if (entry.type
|
|
1050
|
+
if (isStringType(entry.type)) {
|
|
527
1051
|
if (effectiveConstraints.min_length !== undefined && entry.value.length < effectiveConstraints.min_length) {
|
|
528
1052
|
emitError(ctx, createDiag(path, entry.span, `String length violation: expected min length ${effectiveConstraints.min_length}, got ${entry.value.length}`, ErrorCodes.STRING_LENGTH_VIOLATION));
|
|
529
1053
|
}
|
|
530
1054
|
if (effectiveConstraints.max_length !== undefined && entry.value.length > effectiveConstraints.max_length) {
|
|
531
1055
|
emitError(ctx, createDiag(path, entry.span, `String length violation: expected max length ${effectiveConstraints.max_length}, got ${entry.value.length}`, ErrorCodes.STRING_LENGTH_VIOLATION));
|
|
532
1056
|
}
|
|
533
|
-
if (effectiveConstraints.pattern !== undefined && !(
|
|
1057
|
+
if (effectiveConstraints.pattern !== undefined && !patternMatches(effectiveConstraints.pattern, entry.value)) {
|
|
534
1058
|
emitError(ctx, createDiag(path, entry.span, `Pattern mismatch: value does not match ${effectiveConstraints.pattern}`, ErrorCodes.PATTERN_MISMATCH));
|
|
535
1059
|
}
|
|
536
1060
|
}
|
|
@@ -554,7 +1078,13 @@ function mergeDatatypeRuleConstraints(constraints, datatype, datatypeRules) {
|
|
|
554
1078
|
return constraints;
|
|
555
1079
|
return { ...datatypeRule, ...constraints };
|
|
556
1080
|
}
|
|
557
|
-
function constraintTypeMatches(actualType, expectedType, raw) {
|
|
1081
|
+
function constraintTypeMatches(actualType, expectedType, raw, constraints) {
|
|
1082
|
+
if (constraints?.nullable === true && actualType === 'NullLiteral')
|
|
1083
|
+
return true;
|
|
1084
|
+
if (constraints?.allow_infinity === true && actualType === 'InfinityLiteral' && isNumericExpectedType(expectedType))
|
|
1085
|
+
return true;
|
|
1086
|
+
if (constraints?.allow_nan === true && actualType === 'NaNLiteral' && isNumericExpectedType(expectedType))
|
|
1087
|
+
return true;
|
|
558
1088
|
if (actualType === expectedType)
|
|
559
1089
|
return true;
|
|
560
1090
|
if (actualType === 'NumberLiteral') {
|
|
@@ -566,6 +1096,49 @@ function constraintTypeMatches(actualType, expectedType, raw) {
|
|
|
566
1096
|
const satisfies = TYPE_ALIASES[actualType];
|
|
567
1097
|
return Boolean(satisfies?.includes(expectedType));
|
|
568
1098
|
}
|
|
1099
|
+
function isNumericExpectedType(expectedType) {
|
|
1100
|
+
return expectedType === 'NumberLiteral' || expectedType === 'IntegerLiteral' || expectedType === 'FloatLiteral';
|
|
1101
|
+
}
|
|
1102
|
+
function checkLexicalLiteralConstraints(ruleIndex, events, ctx) {
|
|
1103
|
+
for (const [path, rule] of ruleIndex) {
|
|
1104
|
+
const event = events.get(path);
|
|
1105
|
+
if (!event)
|
|
1106
|
+
continue;
|
|
1107
|
+
checkLexicalLiteralConstraint(path, event, rule.constraints, ctx);
|
|
1108
|
+
}
|
|
1109
|
+
}
|
|
1110
|
+
function checkLexicalLiteralConstraint(path, event, constraints, ctx) {
|
|
1111
|
+
if (event.type === 'NullLiteral' && !nullValueMatches(event.value, constraints)) {
|
|
1112
|
+
emitError(ctx, createDiag(path, event.span, `Null value mismatch: expected ${formatExpectedNullValues(constraints)}, got ${event.value || '<none>'}`, ErrorCodes.NULL_VALUE_MISMATCH));
|
|
1113
|
+
}
|
|
1114
|
+
if (event.type === 'ToggleLiteral' && constraints.toggle_pair !== undefined && constraints.toggle_pair !== 'any') {
|
|
1115
|
+
const value = (event.raw || event.value).toLowerCase();
|
|
1116
|
+
const allowed = constraints.toggle_pair === 'yes_no'
|
|
1117
|
+
? ['yes', 'no']
|
|
1118
|
+
: constraints.toggle_pair === 'on_off'
|
|
1119
|
+
? ['on', 'off']
|
|
1120
|
+
: [];
|
|
1121
|
+
if (allowed.length > 0 && !allowed.includes(value)) {
|
|
1122
|
+
emitError(ctx, createDiag(path, event.span, `Toggle pair mismatch: expected ${constraints.toggle_pair}, got ${value || '<none>'}`, ErrorCodes.TOGGLE_PAIR_MISMATCH));
|
|
1123
|
+
}
|
|
1124
|
+
}
|
|
1125
|
+
}
|
|
1126
|
+
function nullValueMatches(value, constraints) {
|
|
1127
|
+
const expected = expectedNullValues(constraints);
|
|
1128
|
+
return expected.length === 0 || expected.includes(value);
|
|
1129
|
+
}
|
|
1130
|
+
function expectedNullValues(constraints) {
|
|
1131
|
+
const values = [];
|
|
1132
|
+
if (constraints.null_value !== undefined)
|
|
1133
|
+
values.push(constraints.null_value);
|
|
1134
|
+
if (constraints.null_values !== undefined)
|
|
1135
|
+
values.push(...constraints.null_values);
|
|
1136
|
+
return values;
|
|
1137
|
+
}
|
|
1138
|
+
function formatExpectedNullValues(constraints) {
|
|
1139
|
+
const values = expectedNullValues(constraints);
|
|
1140
|
+
return values.length > 0 ? values.join(' | ') : '<any>';
|
|
1141
|
+
}
|
|
569
1142
|
function isReferenceType(type) {
|
|
570
1143
|
return type === 'CloneReference' || type === 'PointerReference';
|
|
571
1144
|
}
|
|
@@ -584,17 +1157,115 @@ function datatypeTypeMatches(actualType, expectedType, raw) {
|
|
|
584
1157
|
return true;
|
|
585
1158
|
return false;
|
|
586
1159
|
}
|
|
587
|
-
function
|
|
588
|
-
|
|
1160
|
+
function normalizeRangeLiteral(type, raw) {
|
|
1161
|
+
const normalized = raw.replace(/_/g, '');
|
|
1162
|
+
if (type === 'FloatLiteral' || /[.eE]/.test(normalized)) {
|
|
1163
|
+
if (!/^[+-]?(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][+-]?\d+)?$/.test(normalized))
|
|
1164
|
+
return null;
|
|
1165
|
+
const value = Number(normalized);
|
|
1166
|
+
return Number.isFinite(value) ? { kind: 'float', raw: normalized, value } : null;
|
|
1167
|
+
}
|
|
1168
|
+
if (!/^[+-]?\d+$/.test(normalized))
|
|
589
1169
|
return null;
|
|
590
|
-
return raw
|
|
1170
|
+
return { kind: 'integer', raw: normalized, value: BigInt(normalized) };
|
|
1171
|
+
}
|
|
1172
|
+
function isBelowRange(range, bound) {
|
|
1173
|
+
if (range.kind === 'integer' && /^[-+]?\d+$/.test(bound)) {
|
|
1174
|
+
return range.value < BigInt(bound);
|
|
1175
|
+
}
|
|
1176
|
+
return rangeAsNumber(range) < Number(bound);
|
|
1177
|
+
}
|
|
1178
|
+
function isAboveRange(range, bound) {
|
|
1179
|
+
if (range.kind === 'integer' && /^[-+]?\d+$/.test(bound)) {
|
|
1180
|
+
return range.value > BigInt(bound);
|
|
1181
|
+
}
|
|
1182
|
+
return rangeAsNumber(range) > Number(bound);
|
|
1183
|
+
}
|
|
1184
|
+
function rangeAsNumber(range) {
|
|
1185
|
+
return range.kind === 'integer' ? Number(range.value) : range.value;
|
|
591
1186
|
}
|
|
592
1187
|
function countIntegerDigits(raw) {
|
|
593
1188
|
return raw.replace(/^[+-]/, '').replace(/_/g, '').split('.')[0]?.length ?? 0;
|
|
594
1189
|
}
|
|
1190
|
+
function hasDigitFormConstraints(constraints) {
|
|
1191
|
+
return constraints.sign !== undefined || constraints.min_digits !== undefined || constraints.max_digits !== undefined || constraints.radix !== undefined;
|
|
1192
|
+
}
|
|
1193
|
+
function isDigitFormLiteral(type) {
|
|
1194
|
+
return type === 'NumberLiteral' || type === 'HexLiteral' || type === 'RadixLiteral';
|
|
1195
|
+
}
|
|
1196
|
+
function radixConstraintMatches(datatype, raw, constraints) {
|
|
1197
|
+
if (constraints.radix === undefined)
|
|
1198
|
+
return true;
|
|
1199
|
+
const declaredRadix = declaredRadixFromDatatype(datatype);
|
|
1200
|
+
if (declaredRadix === null && constraints.allow_unspecified_radix !== true)
|
|
1201
|
+
return false;
|
|
1202
|
+
if (declaredRadix !== null && declaredRadix !== constraints.radix)
|
|
1203
|
+
return false;
|
|
1204
|
+
return firstInvalidRadixDigit(raw, constraints.radix) === null;
|
|
1205
|
+
}
|
|
1206
|
+
function declaredRadixFromDatatype(datatype) {
|
|
1207
|
+
if (datatype === undefined)
|
|
1208
|
+
return null;
|
|
1209
|
+
const match = /^radix(?:\[(\d+)\]|(\d+))$/i.exec(datatype.trim());
|
|
1210
|
+
if (!match)
|
|
1211
|
+
return null;
|
|
1212
|
+
const value = Number(match[1] ?? match[2]);
|
|
1213
|
+
return Number.isInteger(value) && value >= 0 ? value : null;
|
|
1214
|
+
}
|
|
1215
|
+
function isStringType(type) {
|
|
1216
|
+
return type === 'StringLiteral'
|
|
1217
|
+
|| type === 'TrimtickLiteral'
|
|
1218
|
+
|| type === 'TrimtickStringLiteral'
|
|
1219
|
+
|| type === 'SeparatorLiteral'
|
|
1220
|
+
|| type === 'NullLiteral'
|
|
1221
|
+
|| type === 'EncodingLiteral'
|
|
1222
|
+
|| type === 'DateLiteral'
|
|
1223
|
+
|| type === 'TimeLiteral'
|
|
1224
|
+
|| type === 'DateTimeLiteral'
|
|
1225
|
+
|| type === 'ZRUTDateTimeLiteral';
|
|
1226
|
+
}
|
|
1227
|
+
function countFormDigits(type, raw) {
|
|
1228
|
+
if (type === 'NumberLiteral')
|
|
1229
|
+
return countIntegerDigits(raw);
|
|
1230
|
+
const body = raw
|
|
1231
|
+
.replace(/^[#%^]/, '')
|
|
1232
|
+
.replace(/^[+-]/, '')
|
|
1233
|
+
.replace(/_/g, '');
|
|
1234
|
+
let count = 0;
|
|
1235
|
+
for (const char of body) {
|
|
1236
|
+
if ((char >= '0' && char <= '9') || ((char >= 'A' && char <= 'Z') || (char >= 'a' && char <= 'z') || char === '&' || char === '!')) {
|
|
1237
|
+
count++;
|
|
1238
|
+
}
|
|
1239
|
+
}
|
|
1240
|
+
return count;
|
|
1241
|
+
}
|
|
1242
|
+
function firstInvalidRadixDigit(raw, radix) {
|
|
1243
|
+
const body = raw.replace(/^%/, '').replace(/^[+-]/, '').replace(/_/g, '');
|
|
1244
|
+
for (const char of body) {
|
|
1245
|
+
const value = radixDigitValue(char);
|
|
1246
|
+
if (value !== null && value >= radix)
|
|
1247
|
+
return char;
|
|
1248
|
+
}
|
|
1249
|
+
return null;
|
|
1250
|
+
}
|
|
1251
|
+
function radixDigitValue(char) {
|
|
1252
|
+
if (char >= '0' && char <= '9')
|
|
1253
|
+
return char.charCodeAt(0) - 48;
|
|
1254
|
+
const lower = char.toLowerCase();
|
|
1255
|
+
if (lower >= 'a' && lower <= 'z')
|
|
1256
|
+
return lower.charCodeAt(0) - 87;
|
|
1257
|
+
if (char === '&')
|
|
1258
|
+
return 36;
|
|
1259
|
+
if (char === '!')
|
|
1260
|
+
return 37;
|
|
1261
|
+
return null;
|
|
1262
|
+
}
|
|
595
1263
|
function isNegative(raw) {
|
|
596
1264
|
return raw.startsWith('-');
|
|
597
1265
|
}
|
|
1266
|
+
function isFormNegative(raw) {
|
|
1267
|
+
return /^[$#%^]?-/.test(raw) || raw.startsWith('-');
|
|
1268
|
+
}
|
|
598
1269
|
function formatCanonicalPathLocal(path) {
|
|
599
1270
|
if (!path || !Array.isArray(path.segments))
|
|
600
1271
|
return '$';
|