@amritk/generate-parsers 0.18.0 → 0.19.0

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.
@@ -1,8 +1,13 @@
1
- import { escapeRegexPattern } from "@amritk/helpers/escape-regex-pattern";
1
+ import { regexLiteral } from "@amritk/helpers/escape-regex-pattern";
2
2
  import { multipleOfPassExpr } from "@amritk/helpers/multiple-of-check";
3
- import { safeAccessor } from "@amritk/helpers/safe-accessor";
4
- import { hasAdditionalProperties, hasConst, hasEnum, hasExclusiveMaximum, hasExclusiveMinimum, hasItems, hasMaxItems, hasMaximum, hasMaxLength, hasMaxProperties, hasMinItems, hasMinimum, hasMinLength, hasMinProperties, hasMultipleOf, hasPattern, hasProperties, hasRequired, hasType, hasUniqueItems, isSchemaObject } from "@amritk/helpers/schema-guards";
3
+ import { resolveRef } from "@amritk/helpers/resolve-ref";
4
+ import { hasOwnCheck, missingCheck, safeAccessor } from "@amritk/helpers/safe-accessor";
5
+ import { hasConst, hasEnum, hasExclusiveMaximum, hasExclusiveMinimum, hasItems, hasMaxItems, hasMaximum, hasMaxLength, hasMaxProperties, hasMinItems, hasMinimum, hasMinLength, hasMinProperties, hasMultipleOf, hasPattern, hasProperties, hasRequired, hasType, hasUniqueItems, isSchemaObject } from "@amritk/helpers/schema-guards";
6
+ import { maxLengthPassExpr, minLengthPassExpr } from "@amritk/helpers/string-length-check";
7
+ import { generateDeepEqualCheck } from "./generate-deep-equal-check.js";
5
8
  import { generateEnumCheck } from "./generate-enum-check.js";
9
+ import { generateUniqueItemsCheck } from "./generate-unique-items-check.js";
10
+ import { unevaluatedItemsExpr, unevaluatedPropertiesExpr } from "./unevaluated-match.js";
6
11
  const HANDLED_KEYWORDS = /* @__PURE__ */ new Set([
7
12
  "type",
8
13
  "enum",
@@ -19,11 +24,30 @@ const HANDLED_KEYWORDS = /* @__PURE__ */ new Set([
19
24
  "maxItems",
20
25
  "uniqueItems",
21
26
  "items",
27
+ "prefixItems",
28
+ "additionalItems",
29
+ "contains",
30
+ "minContains",
31
+ "maxContains",
22
32
  "required",
23
33
  "properties",
34
+ "patternProperties",
35
+ "propertyNames",
36
+ "dependentRequired",
37
+ "dependentSchemas",
24
38
  "minProperties",
25
39
  "maxProperties",
26
- "additionalProperties"
40
+ "additionalProperties",
41
+ "unevaluatedProperties",
42
+ "unevaluatedItems",
43
+ "$ref",
44
+ "allOf",
45
+ "anyOf",
46
+ "oneOf",
47
+ "not",
48
+ "if",
49
+ "then",
50
+ "else"
27
51
  ]);
28
52
  const ANNOTATION_KEYWORDS = /* @__PURE__ */ new Set([
29
53
  "title",
@@ -32,6 +56,7 @@ const ANNOTATION_KEYWORDS = /* @__PURE__ */ new Set([
32
56
  "$id",
33
57
  "$schema",
34
58
  "$anchor",
59
+ "$dynamicAnchor",
35
60
  "$vocabulary",
36
61
  "$defs",
37
62
  "definitions",
@@ -45,16 +70,24 @@ const ANNOTATION_KEYWORDS = /* @__PURE__ */ new Set([
45
70
  "contentEncoding",
46
71
  "contentSchema"
47
72
  ]);
73
+ const depthOf = (ctx) => ctx.depth ?? 0;
74
+ const nest = (ctx) => ({ ...ctx, depth: depthOf(ctx) + 1 });
48
75
  const isObjectExpr = (acc) => `typeof ${acc} === "object" && ${acc} !== null && !Array.isArray(${acc})`;
49
- const hasSchemaAdditionalProperties = (schema) => hasAdditionalProperties(schema) && typeof schema.additionalProperties !== "boolean";
76
+ const recordOf = (acc) => `(${acc} as Record<string, unknown>)`;
77
+ const patternEntriesOf = (schema) => {
78
+ const source = schema["patternProperties"];
79
+ if (typeof source !== "object" || source === null || Array.isArray(source))
80
+ return null;
81
+ return Object.entries(source);
82
+ };
50
83
  const stringConstraints = (acc, schema) => {
51
84
  const c = [];
52
85
  if (hasPattern(schema))
53
- c.push(`/${escapeRegexPattern(schema.pattern)}/.test(${acc})`);
86
+ c.push(`${regexLiteral(schema.pattern)}.test(${acc})`);
54
87
  if (hasMinLength(schema))
55
- c.push(`${acc}.length >= ${schema.minLength}`);
88
+ c.push(minLengthPassExpr(acc, schema.minLength));
56
89
  if (hasMaxLength(schema))
57
- c.push(`${acc}.length <= ${schema.maxLength}`);
90
+ c.push(maxLengthPassExpr(acc, schema.maxLength));
58
91
  return c;
59
92
  };
60
93
  const numberConstraints = (acc, schema, integer) => {
@@ -73,40 +106,103 @@ const numberConstraints = (acc, schema, integer) => {
73
106
  c.push(multipleOfPassExpr(acc, schema.multipleOf));
74
107
  return c;
75
108
  };
76
- const arrayConstraints = (acc, schema, depth) => {
109
+ const tupleShape = (schema) => {
110
+ const s = schema;
111
+ const declared = s["prefixItems"];
112
+ if (Array.isArray(declared))
113
+ return { prefix: declared, tail: s["items"] };
114
+ if (Array.isArray(s["items"]))
115
+ return { prefix: s["items"], tail: s["additionalItems"] };
116
+ return { prefix: null, tail: void 0 };
117
+ };
118
+ const arrayConstraints = (acc, schema, ctx) => {
77
119
  const c = [];
120
+ const d = depthOf(ctx);
121
+ const elements = `(${acc} as unknown[])`;
78
122
  if (hasMinItems(schema))
79
123
  c.push(`${acc}.length >= ${schema.minItems}`);
80
124
  if (hasMaxItems(schema))
81
125
  c.push(`${acc}.length <= ${schema.maxItems}`);
82
126
  if (hasUniqueItems(schema) && schema.uniqueItems === true) {
83
- c.push(`new Set((${acc} as unknown[]).map((_u) => JSON.stringify(_u))).size === ${acc}.length`);
127
+ c.push(generateUniqueItemsCheck(acc, schema));
84
128
  }
85
- if (hasItems(schema)) {
86
- if (Array.isArray(schema.items))
87
- return null;
88
- const el = `_i${depth}`;
89
- const itemMatch = subschemaMatchExpr(el, schema.items, depth + 1);
90
- if (itemMatch === null)
91
- return null;
92
- if (itemMatch !== "true")
93
- c.push(`(${acc} as unknown[]).every((${el}) => ${itemMatch})`);
129
+ const { prefix, tail } = tupleShape(schema);
130
+ if (prefix) {
131
+ for (let i = 0; i < prefix.length; i++) {
132
+ const positional = subschemaMatchExpr(`${elements}[${i}]`, prefix[i], nest(ctx));
133
+ if (positional === null)
134
+ return null;
135
+ if (positional === "true")
136
+ continue;
137
+ c.push(`(${acc}.length <= ${i} || ${positional})`);
138
+ }
139
+ if (tail === false) {
140
+ c.push(`${acc}.length <= ${prefix.length}`);
141
+ } else if (tail !== void 0 && tail !== true) {
142
+ const el = `_i${d}`;
143
+ const tailMatch = subschemaMatchExpr(el, tail, nest(ctx));
144
+ if (tailMatch === null)
145
+ return null;
146
+ if (tailMatch !== "true")
147
+ c.push(`${elements}.every((${el}, _x${d}) => _x${d} < ${prefix.length} || ${tailMatch})`);
148
+ }
149
+ } else if ("items" in schema) {
150
+ if (schema["items"] === false) {
151
+ c.push(`${acc}.length === 0`);
152
+ } else if (hasItems(schema)) {
153
+ const el = `_i${d}`;
154
+ const itemMatch = subschemaMatchExpr(el, schema.items, nest(ctx));
155
+ if (itemMatch === null)
156
+ return null;
157
+ if (itemMatch !== "true")
158
+ c.push(`${elements}.every((${el}) => ${itemMatch})`);
159
+ }
94
160
  }
161
+ const contains = containsConstraint(acc, schema, ctx);
162
+ if (contains === null)
163
+ return null;
164
+ c.push(...contains);
165
+ const unevaluated = unevaluatedItemsExpr(acc, schema, ctx, subschemaMatchExpr);
166
+ if (unevaluated === null)
167
+ return null;
168
+ if (unevaluated !== void 0)
169
+ c.push(unevaluated);
95
170
  return c;
96
171
  };
97
- const objectConstraints = (acc, schema, depth) => {
98
- if (hasSchemaAdditionalProperties(schema))
172
+ const containsConstraint = (acc, schema, ctx) => {
173
+ const s = schema;
174
+ if (!("contains" in s))
175
+ return [];
176
+ const d = depthOf(ctx);
177
+ const min = typeof s["minContains"] === "number" ? s["minContains"] : 1;
178
+ const max = typeof s["maxContains"] === "number" ? s["maxContains"] : void 0;
179
+ if (min === 0 && max === void 0)
180
+ return [];
181
+ const el = `_c${d}`;
182
+ const match = subschemaMatchExpr(el, s["contains"], nest(ctx));
183
+ if (match === null)
99
184
  return null;
185
+ const count = `(${acc} as unknown[]).filter((${el}) => ${match}).length`;
186
+ const bounds = [];
187
+ if (min > 0)
188
+ bounds.push(`${count} >= ${min}`);
189
+ if (max !== void 0)
190
+ bounds.push(`${count} <= ${max}`);
191
+ return bounds;
192
+ };
193
+ const objectConstraints = (acc, schema, ctx) => {
100
194
  const c = [];
195
+ const d = depthOf(ctx);
101
196
  const required = new Set(hasRequired(schema) ? schema.required : []);
197
+ const rec = recordOf(acc);
102
198
  if (hasProperties(schema)) {
103
199
  for (const [key, propSchema] of Object.entries(schema.properties)) {
104
- const propAcc = safeAccessor(acc, key);
105
- const propMatch = subschemaMatchExpr(propAcc, propSchema, depth + 1);
200
+ const propAcc = safeAccessor(rec, key);
201
+ const propMatch = subschemaMatchExpr(propAcc, propSchema, nest(ctx));
106
202
  if (propMatch === null)
107
203
  return null;
108
204
  if (required.has(key)) {
109
- c.push(propMatch === "true" ? `${JSON.stringify(key)} in ${acc}` : `(${JSON.stringify(key)} in ${acc} && ${propMatch})`);
205
+ c.push(propMatch === "true" ? hasOwnCheck(rec, key) : `(${hasOwnCheck(rec, key)} && ${propMatch})`);
110
206
  } else if (propMatch !== "true") {
111
207
  c.push(`(${propAcc} === undefined || ${propMatch})`);
112
208
  }
@@ -114,18 +210,100 @@ const objectConstraints = (acc, schema, depth) => {
114
210
  }
115
211
  }
116
212
  for (const key of required)
117
- c.push(`${JSON.stringify(key)} in ${acc}`);
213
+ c.push(hasOwnCheck(rec, key));
118
214
  if (hasMinProperties(schema))
119
- c.push(`Object.keys(${acc}).length >= ${schema.minProperties}`);
215
+ c.push(`Object.keys(${rec}).length >= ${schema.minProperties}`);
120
216
  if (hasMaxProperties(schema))
121
- c.push(`Object.keys(${acc}).length <= ${schema.maxProperties}`);
122
- if (hasAdditionalProperties(schema) && schema.additionalProperties === false) {
123
- const allowed = JSON.stringify(hasProperties(schema) ? Object.keys(schema.properties) : []);
124
- c.push(`Object.keys(${acc}).every((_k) => ${allowed}.includes(_k))`);
217
+ c.push(`Object.keys(${rec}).length <= ${schema.maxProperties}`);
218
+ const keyTerms = keyWalkTerms(`_k${d}`, `_v${d}`, schema, ctx);
219
+ if (keyTerms === null)
220
+ return null;
221
+ if (keyTerms.length > 0) {
222
+ const needsValue = keyTerms.some((term) => term.usesValue);
223
+ const body = keyTerms.map((term) => term.expr).join(" && ");
224
+ c.push(needsValue ? `Object.keys(${rec}).every((_k${d}) => { const _v${d} = ${rec}[_k${d}]; return ${body}; })` : `Object.keys(${rec}).every((_k${d}) => ${body})`);
125
225
  }
226
+ const dependent = dependentTerms(rec, acc, schema, ctx);
227
+ if (dependent === null)
228
+ return null;
229
+ c.push(...dependent);
230
+ const unevaluated = unevaluatedPropertiesExpr(acc, schema, ctx, subschemaMatchExpr);
231
+ if (unevaluated === null)
232
+ return null;
233
+ if (unevaluated !== void 0)
234
+ c.push(unevaluated);
126
235
  return c;
127
236
  };
128
- const subschemaMatchExpr = (accessor, schema, depth = 0) => {
237
+ const keyWalkTerms = (keyVar, valueVar, schema, ctx) => {
238
+ const terms = [];
239
+ const patterns = patternEntriesOf(schema);
240
+ const declared = hasProperties(schema) ? Object.keys(schema.properties) : [];
241
+ if (patterns) {
242
+ for (const [pattern, sub] of patterns) {
243
+ const match = subschemaMatchExpr(valueVar, sub, nest(ctx));
244
+ if (match === null)
245
+ return null;
246
+ if (match === "true")
247
+ continue;
248
+ terms.push({ expr: `(!${regexLiteral(pattern)}.test(${keyVar}) || ${match})`, usesValue: true });
249
+ }
250
+ }
251
+ const additional = schema["additionalProperties"];
252
+ if (additional !== void 0 && additional !== true) {
253
+ const guards = [];
254
+ if (declared.length > 0)
255
+ guards.push(`!${JSON.stringify(declared)}.includes(${keyVar})`);
256
+ if (patterns)
257
+ for (const [pattern] of patterns)
258
+ guards.push(`!${regexLiteral(pattern)}.test(${keyVar})`);
259
+ const unclaimed = guards.length > 0 ? guards.join(" && ") : null;
260
+ if (additional === false) {
261
+ terms.push({ expr: unclaimed === null ? "false" : `!(${unclaimed})`, usesValue: false });
262
+ } else {
263
+ const match = subschemaMatchExpr(valueVar, additional, nest(ctx));
264
+ if (match === null)
265
+ return null;
266
+ if (match !== "true") {
267
+ terms.push({ expr: unclaimed === null ? match : `(!(${unclaimed}) || ${match})`, usesValue: true });
268
+ }
269
+ }
270
+ }
271
+ const names = schema["propertyNames"];
272
+ if (names !== void 0) {
273
+ const match = subschemaMatchExpr(keyVar, names, nest(ctx));
274
+ if (match === null)
275
+ return null;
276
+ if (match !== "true")
277
+ terms.push({ expr: match, usesValue: false });
278
+ }
279
+ return terms;
280
+ };
281
+ const dependentTerms = (rec, acc, schema, ctx) => {
282
+ const s = schema;
283
+ const terms = [];
284
+ const dependentRequired = s["dependentRequired"];
285
+ if (typeof dependentRequired === "object" && dependentRequired !== null && !Array.isArray(dependentRequired)) {
286
+ for (const [trigger, deps] of Object.entries(dependentRequired)) {
287
+ if (!Array.isArray(deps) || deps.length === 0)
288
+ continue;
289
+ const present = deps.map((dep) => hasOwnCheck(rec, dep)).join(" && ");
290
+ terms.push(`(${missingCheck(rec, trigger)} || ${deps.length === 1 ? present : `(${present})`})`);
291
+ }
292
+ }
293
+ const dependentSchemas = s["dependentSchemas"];
294
+ if (typeof dependentSchemas === "object" && dependentSchemas !== null && !Array.isArray(dependentSchemas)) {
295
+ for (const [trigger, sub] of Object.entries(dependentSchemas)) {
296
+ const match = subschemaMatchExpr(acc, sub, nest(ctx));
297
+ if (match === null)
298
+ return null;
299
+ if (match === "true")
300
+ continue;
301
+ terms.push(`(${missingCheck(rec, trigger)} || ${match})`);
302
+ }
303
+ }
304
+ return terms;
305
+ };
306
+ const subschemaMatchExpr = (accessor, schema, ctx = {}) => {
129
307
  if (schema === true)
130
308
  return "true";
131
309
  if (schema === false)
@@ -136,27 +314,41 @@ const subschemaMatchExpr = (accessor, schema, depth = 0) => {
136
314
  if (!HANDLED_KEYWORDS.has(key) && !ANNOTATION_KEYWORDS.has(key))
137
315
  return null;
138
316
  }
139
- if ("type" in schema && typeof schema.type !== "string")
317
+ if (depthOf(ctx) > 8)
140
318
  return null;
141
319
  const checks = [];
142
- if (hasConst(schema)) {
143
- const c = schema.const;
144
- if (c !== null && typeof c === "object")
145
- return null;
146
- checks.push(`${accessor} === ${JSON.stringify(c)}`);
147
- }
320
+ const refCheck = refMatch(accessor, schema, ctx);
321
+ if (refCheck === null)
322
+ return null;
323
+ if (refCheck !== "true")
324
+ checks.push(refCheck);
325
+ const combinators = combinatorChecks(accessor, schema, ctx);
326
+ if (combinators === null)
327
+ return null;
328
+ checks.push(...combinators);
329
+ if (hasConst(schema))
330
+ checks.push(`(${generateDeepEqualCheck(accessor, schema.const)})`);
148
331
  if (hasEnum(schema)) {
149
332
  if (schema.enum.length === 0)
150
- return null;
333
+ return "false";
151
334
  checks.push(generateEnumCheck(accessor, schema.enum));
152
335
  }
153
336
  if (hasType(schema)) {
154
- const typeChecks = buildTypedChecks(accessor, schema, depth);
337
+ const typeChecks = buildTypedChecks(accessor, schema, ctx);
155
338
  if (typeChecks === null)
156
339
  return null;
157
340
  checks.push(...typeChecks);
341
+ } else if (Array.isArray(schema.type)) {
342
+ const branches = [];
343
+ for (const type of schema.type) {
344
+ const branch = buildTypedChecks(accessor, { ...schema, type }, ctx);
345
+ if (branch === null)
346
+ return null;
347
+ branches.push(branch.length === 1 ? branch[0] : `(${branch.join(" && ")})`);
348
+ }
349
+ checks.push(branches.length === 0 ? "false" : branches.length === 1 ? branches[0] : `(${branches.join(" || ")})`);
158
350
  } else {
159
- const typeless = buildTypelessChecks(accessor, schema, depth);
351
+ const typeless = buildTypelessChecks(accessor, schema, ctx);
160
352
  if (typeless === null)
161
353
  return null;
162
354
  checks.push(...typeless);
@@ -165,7 +357,73 @@ const subschemaMatchExpr = (accessor, schema, depth = 0) => {
165
357
  return "true";
166
358
  return checks.length === 1 ? checks[0] : `(${checks.join(" && ")})`;
167
359
  };
168
- const buildTypedChecks = (accessor, schema, depth) => {
360
+ const refMatch = (accessor, schema, ctx) => {
361
+ const ref = schema["$ref"];
362
+ if (typeof ref !== "string")
363
+ return "true";
364
+ if (ctx.rootSchema === void 0)
365
+ return null;
366
+ if (ctx.seen?.has(ref))
367
+ return null;
368
+ const target = resolveRef(ref, ctx.rootSchema);
369
+ if (target === void 0)
370
+ return null;
371
+ const seen = new Set(ctx.seen ?? []);
372
+ seen.add(ref);
373
+ return subschemaMatchExpr(accessor, target, { ...nest(ctx), seen });
374
+ };
375
+ const combinatorChecks = (accessor, schema, ctx) => {
376
+ const record = schema;
377
+ const checks = [];
378
+ for (const keyword of ["allOf", "anyOf", "oneOf"]) {
379
+ const members = record[keyword];
380
+ if (members === void 0)
381
+ continue;
382
+ if (!Array.isArray(members) || members.length === 0)
383
+ return null;
384
+ const matches = [];
385
+ for (const member of members) {
386
+ const match = subschemaMatchExpr(accessor, member, nest(ctx));
387
+ if (match === null)
388
+ return null;
389
+ matches.push(match);
390
+ }
391
+ if (keyword === "allOf") {
392
+ const constraining = matches.filter((match) => match !== "true");
393
+ if (constraining.length > 0)
394
+ checks.push(joinAnd(constraining));
395
+ } else if (keyword === "anyOf") {
396
+ if (matches.some((match) => match === "true"))
397
+ continue;
398
+ checks.push(matches.length === 1 ? matches[0] : `(${matches.join(" || ")})`);
399
+ } else {
400
+ checks.push(matches.length === 1 ? matches[0] : `((${matches.map((match) => `(${match} ? 1 : 0)`).join(" + ")}) === 1)`);
401
+ }
402
+ }
403
+ if ("not" in record) {
404
+ const match = subschemaMatchExpr(accessor, record["not"], nest(ctx));
405
+ if (match === null)
406
+ return null;
407
+ if (match !== "false")
408
+ checks.push(`!(${match})`);
409
+ }
410
+ if ("if" in record) {
411
+ const ifMatch = subschemaMatchExpr(accessor, record["if"], nest(ctx));
412
+ if (ifMatch === null)
413
+ return null;
414
+ const branch = (keyword) => keyword in record ? subschemaMatchExpr(accessor, record[keyword], nest(ctx)) : void 0;
415
+ const thenMatch = branch("then");
416
+ const elseMatch = branch("else");
417
+ if (thenMatch === null || elseMatch === null)
418
+ return null;
419
+ if (thenMatch !== void 0 && thenMatch !== "true")
420
+ checks.push(`(!(${ifMatch}) || ${thenMatch})`);
421
+ if (elseMatch !== void 0 && elseMatch !== "true")
422
+ checks.push(`((${ifMatch}) || ${elseMatch})`);
423
+ }
424
+ return checks;
425
+ };
426
+ const buildTypedChecks = (accessor, schema, ctx) => {
169
427
  switch (schema.type) {
170
428
  case "string":
171
429
  return [`typeof ${accessor} === "string"`, ...stringConstraints(accessor, schema)];
@@ -178,13 +436,13 @@ const buildTypedChecks = (accessor, schema, depth) => {
178
436
  case "null":
179
437
  return [`${accessor} === null`];
180
438
  case "array": {
181
- const constraints = arrayConstraints(accessor, schema, depth);
439
+ const constraints = arrayConstraints(accessor, schema, ctx);
182
440
  if (constraints === null)
183
441
  return null;
184
442
  return [`Array.isArray(${accessor})`, ...constraints];
185
443
  }
186
444
  case "object": {
187
- const constraints = objectConstraints(accessor, schema, depth);
445
+ const constraints = objectConstraints(accessor, schema, ctx);
188
446
  if (constraints === null)
189
447
  return null;
190
448
  return [isObjectExpr(accessor), ...constraints];
@@ -193,7 +451,7 @@ const buildTypedChecks = (accessor, schema, depth) => {
193
451
  return null;
194
452
  }
195
453
  };
196
- const buildTypelessChecks = (accessor, schema, depth) => {
454
+ const buildTypelessChecks = (accessor, schema, ctx) => {
197
455
  const checks = [];
198
456
  const strChecks = stringConstraints(accessor, schema);
199
457
  if (strChecks.length > 0)
@@ -201,12 +459,12 @@ const buildTypelessChecks = (accessor, schema, depth) => {
201
459
  const numChecks = numberConstraints(accessor, schema, false);
202
460
  if (numChecks.length > 0)
203
461
  checks.push(`(typeof ${accessor} !== "number" || ${joinAnd(numChecks)})`);
204
- const arrChecks = arrayConstraints(accessor, schema, depth);
462
+ const arrChecks = arrayConstraints(accessor, schema, ctx);
205
463
  if (arrChecks === null)
206
464
  return null;
207
465
  if (arrChecks.length > 0)
208
466
  checks.push(`(!Array.isArray(${accessor}) || ${joinAnd(arrChecks)})`);
209
- const objChecks = objectConstraints(accessor, schema, depth);
467
+ const objChecks = objectConstraints(accessor, schema, ctx);
210
468
  if (objChecks === null)
211
469
  return null;
212
470
  if (objChecks.length > 0)
@@ -214,8 +472,14 @@ const buildTypelessChecks = (accessor, schema, depth) => {
214
472
  return checks;
215
473
  };
216
474
  const joinAnd = (checks) => checks.length === 1 ? checks[0] : `(${checks.join(" && ")})`;
217
- const canMatchSubschema = (schema) => subschemaMatchExpr("_x", schema) !== null;
475
+ const hasConstrainingRefSibling = (schema) => {
476
+ if (!isSchemaObject(schema) || !("$ref" in schema))
477
+ return false;
478
+ return Object.keys(schema).some((key) => key !== "$ref" && !ANNOTATION_KEYWORDS.has(key));
479
+ };
480
+ const canMatchSubschema = (schema, rootSchema) => subschemaMatchExpr("_x", schema, rootSchema ? { rootSchema } : {}) !== null;
218
481
  export {
219
482
  canMatchSubschema,
483
+ hasConstrainingRefSibling,
220
484
  subschemaMatchExpr
221
485
  };
@@ -0,0 +1,19 @@
1
+ import type { JSONSchema } from 'json-schema-typed/draft-2020-12';
2
+ import type { MatchContext } from './subschema-match.js';
3
+ type MatchFn = (accessor: string, schema: JSONSchema, ctx: MatchContext) => string | null;
4
+ /**
5
+ * The `unevaluatedProperties` conjunct for an object accessor: every own key the
6
+ * schema's other keywords did not evaluate must satisfy the unevaluated
7
+ * subschema (and `false` means there must be no such key).
8
+ *
9
+ * Returns `undefined` when the keyword is absent or constrains nothing (a `true`
10
+ * schema, or a node whose other keywords already sweep every key), and `null`
11
+ * when it cannot be proven inline.
12
+ */
13
+ export declare const unevaluatedPropertiesExpr: (acc: string, schema: JSONSchema, ctx: MatchContext, match: MatchFn) => string | null | undefined;
14
+ /**
15
+ * The `unevaluatedItems` conjunct for an array accessor — the index counterpart
16
+ * of {@link unevaluatedPropertiesExpr}.
17
+ */
18
+ export declare const unevaluatedItemsExpr: (acc: string, schema: JSONSchema, ctx: MatchContext, match: MatchFn) => string | null | undefined;
19
+ export {};