@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.
@@ -41,13 +41,26 @@ export declare const isInlineObjectArrayProperty: (propSchema: JSONSchema) => bo
41
41
  * turn into a membership check).
42
42
  */
43
43
  export declare const getUnionBranches: (schema: JSONSchema) => readonly JSONSchema[] | null;
44
+ /**
45
+ * True when {@link getUnionBranches} came from a `oneOf` rather than an `anyOf` —
46
+ * i.e. the value must match *exactly one* branch, not at least one.
47
+ *
48
+ * The distinction was dropped before: both spellings produced a plain
49
+ * disjunction, so `{ oneOf: [{…a}, {…b}] }` accepted `{ a: 'x', b: 1 }`, which
50
+ * matches both branches. Ajv, the runtime interpreter, and `generate-validators`
51
+ * all reject it, so the two mjst engines disagreed on the same contract.
52
+ */
53
+ export declare const isExclusiveUnion: (schema: JSONSchema) => boolean;
44
54
  /**
45
55
  * True when a schema carries a keyword the strict slow path enforces but which
46
56
  * no flat type check can mirror: `contains` (array item counting),
47
- * `dependentRequired` / `dependentSchemas` (cross-property presence), or
48
- * `propertyNames` (per-key constraints). A fast path or shape validator that
49
- * accepted such a value on a bare type check would skip the enforcement, so
50
- * both must bail when this is true — the slow path then runs the real checks.
57
+ * `dependentRequired` / `dependentSchemas` (cross-property presence),
58
+ * `propertyNames` (per-key constraints), the conditional and composition
59
+ * keywords (`if`/`then`/`else`, `not`, a constraining `allOf`), or per-key value
60
+ * schemas (`patternProperties`, a constraining `additionalProperties`). A fast
61
+ * path or shape validator that accepted such a value on a bare type check would
62
+ * skip the enforcement, so both must bail when this is true — the slow path then
63
+ * runs the real checks.
51
64
  */
52
65
  export declare const hasFastPathBlockingKeyword: (schema: JSONSchema) => boolean;
53
66
  /**
@@ -83,13 +96,21 @@ export declare const generatePropertyTypeCheck: (varName: string, schema: JSONSc
83
96
  */
84
97
  export declare const generateInlineObjectCheck: (varName: string, schema: JSONSchema, useRefImports: boolean, suffix: string) => string | null;
85
98
  /**
86
- * Generates a union membership check: true when `varName` matches at least one
87
- * branch of a `oneOf`/`anyOf`. Inline object branches get a per-property check;
88
- * everything else goes through {@link generatePropertyTypeCheck} ($refs call
89
- * the imported shape validator, consts/enums/scalars check directly). Returns
90
- * null when any branch cannot be checked.
99
+ * Generates a union membership check. Inline object branches get a per-property
100
+ * check; everything else goes through {@link generatePropertyTypeCheck} ($refs
101
+ * call the imported shape validator, consts/enums/scalars check directly).
102
+ * Returns null when any branch cannot be checked.
103
+ *
104
+ * `exclusive` (a `oneOf` — see {@link isExclusiveUnion}) counts the matching
105
+ * branches and demands exactly one, instead of the `anyOf` disjunction. The count
106
+ * form is *tighter* than the disjunction it replaces — anything it accepts, the
107
+ * disjunction accepted too — so a true-sound caller (a fast path, a shape
108
+ * validator) can only get safer: a value it now declines merely takes the slow
109
+ * path, which re-validates. A caller that *throws* on a false result needs each
110
+ * branch check to be exact in both directions, which is precisely what
111
+ * {@link canEnforceUnion} certifies.
91
112
  */
92
- export declare const generateUnionCheck: (varName: string, branches: readonly JSONSchema[], useRefImports: boolean, suffix: string) => string | null;
113
+ export declare const generateUnionCheck: (varName: string, branches: readonly JSONSchema[], useRefImports: boolean, suffix: string, exclusive?: boolean) => string | null;
93
114
  /**
94
115
  * True when the union membership check emitted by {@link generateUnionCheck}
95
116
  * is also *false-sound* — a false result definitely means the value is invalid
@@ -1,11 +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
3
  import { refToName } from "@amritk/helpers/ref-to-name";
4
4
  import { resolveRef } from "@amritk/helpers/resolve-ref";
5
- import { safeAccessor } from "@amritk/helpers/safe-accessor";
6
- import { hasAdditionalProperties, hasAllOf, hasAnyOf, hasConst, hasEnum, hasExclusiveMaximum, hasExclusiveMinimum, hasItems, hasMaxItems, hasMaximum, hasMaxLength, hasMinItems, hasMinimum, hasMinLength, hasMultipleOf, hasOneOf, hasPattern, hasProperties, hasRef, hasType, hasUniqueItems, isObjectSchema, isSchemaObject } from "@amritk/helpers/schema-guards";
5
+ import { hasOwnCheck, safeAccessor } from "@amritk/helpers/safe-accessor";
6
+ import { hasAdditionalProperties, hasAllOf, hasAnyOf, hasConst, hasEnum, hasExclusiveMaximum, hasExclusiveMinimum, hasItems, hasMaxItems, hasMaximum, hasMaxLength, hasMaxProperties, hasMinItems, hasMinimum, hasMinLength, hasMinProperties, hasMultipleOf, hasOneOf, hasPattern, hasProperties, hasRef, hasType, hasUniqueItems, isObjectSchema, isSchemaObject } from "@amritk/helpers/schema-guards";
7
7
  import { generateEnumCheck } from "./generate-enum-check.js";
8
- import { getPrefixItems, prefixItemsCapsLength, scalarItemTypeCheck, singleTypeCheck } from "./generate-validation-expression.js";
8
+ import { generateUniqueItemsCheck } from "./generate-unique-items-check.js";
9
+ import { everyTailItem, getPrefixItems, prefixItemsCapsLength, scalarItemTypeCheck, singleTypeCheck } from "./generate-validation-expression.js";
10
+ import { hasConstrainingRefSibling, subschemaMatchExpr } from "./subschema-match.js";
9
11
  const shapeValidatorName = (typeName) => `validate${typeName}Shape`;
10
12
  const isInlineObjectProperty = (propSchema) => {
11
13
  if (!isSchemaObject(propSchema))
@@ -44,11 +46,30 @@ const getUnionBranches = (schema) => {
44
46
  return schema.anyOf;
45
47
  return null;
46
48
  };
49
+ const isExclusiveUnion = (schema) => isSchemaObject(schema) && !hasAllOf(schema) && !("not" in schema) && hasOneOf(schema) && schema.oneOf.length > 0;
47
50
  const hasFastPathBlockingKeyword = (schema) => {
48
51
  if (!isSchemaObject(schema))
49
52
  return false;
50
53
  const record = schema;
51
- return "contains" in record || "dependentSchemas" in record || "propertyNames" in record || "dependentRequired" in record;
54
+ return "contains" in record || "dependentSchemas" in record || "propertyNames" in record || "dependentRequired" in record || // Composition and conditionals: the strict assertions now enforce these, but
55
+ // a guard built from per-property type checks cannot mirror them — it said
56
+ // "already in shape" for `{ kind: 'a' }` under `if: {kind: 'a'}, then:
57
+ // {required: ['n']}` and returned before the conditional was ever tested.
58
+ "if" in record || "then" in record || "else" in record || "not" in record || "patternProperties" in record || // `unevaluated*` is answered against the whole node's annotation coverage,
59
+ // which a per-property guard has no way to reproduce.
60
+ "unevaluatedProperties" in record && record["unevaluatedProperties"] !== true || "unevaluatedItems" in record && record["unevaluatedItems"] !== true || // A `$ref` guard calls the target's shape validator, which knows nothing
61
+ // about the siblings 2020-12 still applies: `{ $ref: …, minProperties: 2 }`
62
+ // was waved through on the target's shape alone.
63
+ hasConstrainingRefSibling(schema) || hasConstrainingAllOf(schema) || hasConstrainingAdditionalProperties(schema);
64
+ };
65
+ const hasConstrainingAllOf = (schema) => hasAllOf(schema) && schema.allOf.some((member) => !(isSchemaObject(member) && hasRef(member)));
66
+ const hasConstrainingAdditionalProperties = (schema) => {
67
+ if (!hasAdditionalProperties(schema))
68
+ return false;
69
+ const additional = schema.additionalProperties;
70
+ if (typeof additional === "boolean")
71
+ return false;
72
+ return subschemaMatchExpr("_x", additional) !== "true";
52
73
  };
53
74
  const CONSTRAINT_KEYWORDS = [
54
75
  "pattern",
@@ -91,10 +112,8 @@ const multiTypeCheck = (varName, schema, options = {}) => {
91
112
  const generatePropertyTypeCheck = (varName, schema, useRefImports, suffix) => {
92
113
  if (!isSchemaObject(schema))
93
114
  return null;
94
- const record = schema;
95
- if ("contains" in record || "dependentSchemas" in record || "propertyNames" in record || "dependentRequired" in record) {
115
+ if (hasFastPathBlockingKeyword(schema))
96
116
  return null;
97
- }
98
117
  if (useRefImports && hasRef(schema)) {
99
118
  const refName = refToName(schema.$ref, suffix);
100
119
  return `${shapeValidatorName(refName)}(${varName})`;
@@ -106,7 +125,7 @@ const generatePropertyTypeCheck = (varName, schema, useRefImports, suffix) => {
106
125
  }
107
126
  const branches = getUnionBranches(schema);
108
127
  if (branches) {
109
- return generateUnionCheck(varName, branches, useRefImports, suffix);
128
+ return generateUnionCheck(varName, branches, useRefImports, suffix, isExclusiveUnion(schema));
110
129
  }
111
130
  if (hasOneOf(schema) || hasAnyOf(schema) || hasAllOf(schema) || "not" in schema) {
112
131
  return null;
@@ -129,7 +148,7 @@ const generatePropertyTypeCheck = (varName, schema, useRefImports, suffix) => {
129
148
  case "string": {
130
149
  checks.push(`typeof ${varName} === "string"`);
131
150
  if (hasPattern(schema))
132
- checks.push(`/${escapeRegexPattern(schema.pattern)}/.test(${varName})`);
151
+ checks.push(`${regexLiteral(schema.pattern)}.test(${varName})`);
133
152
  if (hasMinLength(schema))
134
153
  checks.push(`${varName}.length >= ${schema.minLength}`);
135
154
  if (hasMaxLength(schema))
@@ -163,13 +182,20 @@ const generatePropertyTypeCheck = (varName, schema, useRefImports, suffix) => {
163
182
  if (hasMaxItems(schema))
164
183
  checks.push(`${varName}.length <= ${schema.maxItems}`);
165
184
  if (hasUniqueItems(schema) && schema.uniqueItems === true) {
166
- checks.push(`new Set(${varName}).size === ${varName}.length`);
185
+ checks.push(generateUniqueItemsCheck(varName, schema));
167
186
  }
168
187
  if (hasItems(schema) && !Array.isArray(schema.items)) {
169
188
  const items = schema.items;
170
189
  const itemCheck = scalarItemTypeCheck(items, "_it") ?? (isSchemaObject(items) && hasEnum(items) && items.enum.length > 0 ? generateEnumCheck("_it", items.enum) : null);
171
- if (itemCheck)
172
- checks.push(`${varName}.every((_it) => ${itemCheck})`);
190
+ if (itemCheck) {
191
+ checks.push(everyTailItem(varName, itemCheck, schema));
192
+ } else if (!(isSchemaObject(items) && (hasRef(items) || isInlineObjectProperty(items)))) {
193
+ const match = subschemaMatchExpr("_it", items);
194
+ if (match === null)
195
+ return null;
196
+ if (match !== "true")
197
+ checks.push(everyTailItem(varName, match, schema));
198
+ }
173
199
  }
174
200
  const prefix = getPrefixItems(schema);
175
201
  if (prefix) {
@@ -186,6 +212,10 @@ const generatePropertyTypeCheck = (varName, schema, useRefImports, suffix) => {
186
212
  }
187
213
  case "object":
188
214
  checks.push(`isObject(${varName})`);
215
+ if (hasMinProperties(schema))
216
+ checks.push(`Object.keys(${varName}).length >= ${schema.minProperties}`);
217
+ if (hasMaxProperties(schema))
218
+ checks.push(`Object.keys(${varName}).length <= ${schema.maxProperties}`);
189
219
  break;
190
220
  default:
191
221
  return null;
@@ -218,15 +248,19 @@ const generateInlineObjectCheck = (varName, schema, useRefImports, suffix) => {
218
248
  required.delete(key);
219
249
  }
220
250
  for (const key of required) {
221
- checks.push(`${JSON.stringify(key)} in ${varName}`);
251
+ checks.push(hasOwnCheck(varName, key));
222
252
  }
223
253
  if (hasAdditionalProperties(schema) && schema.additionalProperties === false) {
224
254
  const allowedKeys = JSON.stringify(Object.keys(schema.properties));
225
255
  checks.push(`Object.keys(${varName}).every((_k) => ${allowedKeys}.includes(_k))`);
226
256
  }
257
+ if (hasMinProperties(schema))
258
+ checks.push(`Object.keys(${varName}).length >= ${schema.minProperties}`);
259
+ if (hasMaxProperties(schema))
260
+ checks.push(`Object.keys(${varName}).length <= ${schema.maxProperties}`);
227
261
  return checks.join(" && ");
228
262
  };
229
- const generateUnionCheck = (varName, branches, useRefImports, suffix) => {
263
+ const generateUnionCheck = (varName, branches, useRefImports, suffix, exclusive = false) => {
230
264
  if (branches.length === 0)
231
265
  return null;
232
266
  const parts = [];
@@ -238,7 +272,11 @@ const generateUnionCheck = (varName, branches, useRefImports, suffix) => {
238
272
  return null;
239
273
  parts.push(`(${check})`);
240
274
  }
241
- return parts.length === 1 ? parts[0] : `(${parts.join(" || ")})`;
275
+ if (parts.length === 1)
276
+ return parts[0];
277
+ if (exclusive)
278
+ return `((${parts.map((part) => `(${part} ? 1 : 0)`).join(" + ")}) === 1)`;
279
+ return `(${parts.join(" || ")})`;
242
280
  };
243
281
  const canEnforceUnion = (branches, rootSchema) => {
244
282
  const visiting = /* @__PURE__ */ new Set();
@@ -302,6 +340,13 @@ const canTrustShapeProperty = (propSchema, rootSchema, visiting) => {
302
340
  }
303
341
  return canTrustPropertyCheck(propSchema, rootSchema, visiting);
304
342
  };
343
+ const KEYWORDS_INLINE_CHECK_IGNORES = [
344
+ "propertyNames",
345
+ "dependentRequired",
346
+ "dependentSchemas",
347
+ "contains",
348
+ "unevaluatedProperties"
349
+ ];
305
350
  const canTrustInlineObjectCheck = (schema, rootSchema, visiting) => {
306
351
  if (!isSchemaObject(schema) || !isObjectSchema(schema) || !hasProperties(schema))
307
352
  return false;
@@ -311,6 +356,9 @@ const canTrustInlineObjectCheck = (schema, rootSchema, visiting) => {
311
356
  return false;
312
357
  if (hasAdditionalProperties(schema) && typeof schema.additionalProperties !== "boolean")
313
358
  return false;
359
+ const record = schema;
360
+ if (KEYWORDS_INLINE_CHECK_IGNORES.some((keyword) => keyword in record))
361
+ return false;
314
362
  return Object.values(schema.properties).every((propSchema) => canTrustPropertyCheck(propSchema, rootSchema, visiting));
315
363
  };
316
364
  const canTrustReferencedValidator = (ref, rootSchema, visiting) => {
@@ -353,6 +401,7 @@ export {
353
401
  generateUnionCheck,
354
402
  getUnionBranches,
355
403
  hasFastPathBlockingKeyword,
404
+ isExclusiveUnion,
356
405
  isInlineObjectArrayProperty,
357
406
  isInlineObjectProperty,
358
407
  multiTypeCheck,
@@ -0,0 +1,16 @@
1
+ import type { JSONSchema } from 'json-schema-typed/draft-2020-12';
2
+ /**
3
+ * Builds the boolean expression that is TRUE when every element of `accessor` is
4
+ * distinct — the `uniqueItems: true` contract.
5
+ *
6
+ * Scalar-only arrays take the cheap native `Set` (exact for primitives, and
7
+ * allocation is a single pass). Anything that might hold an object or an array
8
+ * falls back to the canonical-JSON projection above, because JSON Schema compares
9
+ * items by *structural* equality and neither a plain `Set` (reference equality)
10
+ * nor a raw `JSON.stringify` key (order sensitive) answers that question.
11
+ *
12
+ * @param accessor - Expression yielding the array; the caller must already have
13
+ * proven `Array.isArray`.
14
+ * @param schema - The array schema, read for its item types.
15
+ */
16
+ export declare const generateUniqueItemsCheck: (accessor: string, schema: JSONSchema) => string;
@@ -0,0 +1,32 @@
1
+ import { hasType, isSchemaObject } from "@amritk/helpers/schema-guards";
2
+ const SCALAR_TYPES = /* @__PURE__ */ new Set(["string", "number", "integer", "boolean", "null"]);
3
+ const schemaIsScalarOnly = (schema) => {
4
+ if (schema === void 0 || !isSchemaObject(schema) || !hasType(schema))
5
+ return false;
6
+ const type = schema["type"];
7
+ const types = Array.isArray(type) ? type : [type];
8
+ return types.length > 0 && types.every((t) => typeof t === "string" && SCALAR_TYPES.has(t));
9
+ };
10
+ const arrayItemsAreScalarOnly = (schema) => {
11
+ if (!isSchemaObject(schema))
12
+ return false;
13
+ const record = schema;
14
+ const prefix = record["prefixItems"];
15
+ if (Array.isArray(prefix)) {
16
+ if (!prefix.every((position) => schemaIsScalarOnly(position)))
17
+ return false;
18
+ const tail = "items" in record ? record["items"] : record["additionalItems"];
19
+ return tail === false ? true : schemaIsScalarOnly(tail);
20
+ }
21
+ if (Array.isArray(record["items"])) {
22
+ if (!record["items"].every((position) => schemaIsScalarOnly(position)))
23
+ return false;
24
+ return record["additionalItems"] === false ? true : schemaIsScalarOnly(record["additionalItems"]);
25
+ }
26
+ return schemaIsScalarOnly(record["items"]);
27
+ };
28
+ const CANONICAL_JSON = 'JSON.stringify(_u, (_k, _v) => (_v !== null && typeof _v === "object" && !Array.isArray(_v)) ? Object.fromEntries(Object.keys(_v).sort().map((_sk) => [_sk, (_v as Record<string, unknown>)[_sk]])) : _v)';
29
+ const generateUniqueItemsCheck = (accessor, schema) => arrayItemsAreScalarOnly(schema) ? `new Set(${accessor}).size === ${accessor}.length` : `new Set((${accessor} as unknown[]).map((_u) => ${CANONICAL_JSON})).size === ${accessor}.length`;
30
+ export {
31
+ generateUniqueItemsCheck
32
+ };
@@ -21,6 +21,22 @@ export declare const isCoercibleItemSchema: (itemSchema: JSONSchema) => boolean;
21
21
  * here rather than left unchecked.
22
22
  */
23
23
  export declare const getPrefixItems: (schema: JSONSchema) => readonly JSONSchema[] | null;
24
+ /**
25
+ * Wraps a per-element boolean check into the `.every(...)` call that proves it
26
+ * for a whole array, skipping the positions `prefixItems` already owns.
27
+ *
28
+ * In 2020-12 `items` is only the *tail* schema: it applies from
29
+ * `prefixItems.length` onward. Checking every element against it made
30
+ * `{ prefixItems: [{type:'string'}], items: {type:'number'} }` reject
31
+ * `["a", 1, 2]` — a value Ajv accepts, and one the `[string?, ...number[]]` type
32
+ * this generator emits admits, so the strict parser threw on its own declared
33
+ * type. The index guard is short-circuited rather than sliced so the check stays
34
+ * allocation-free on the hot path.
35
+ *
36
+ * The draft-07 tuple spelling needs no guard: there `items` *is* the tuple and
37
+ * the tail lives in `additionalItems`, which no item check reads.
38
+ */
39
+ export declare const everyTailItem: (accessor: string, itemCheck: string, schema: JSONSchema) => string;
24
40
  /**
25
41
  * True when a sibling `items: false` (2020-12) or `additionalItems: false`
26
42
  * (draft) caps a tuple's length — any element past the declared prefix is
@@ -1,8 +1,8 @@
1
- import { escapeRegexPattern } from "@amritk/helpers/escape-regex-pattern";
1
+ import { regexLiteral } from "@amritk/helpers/escape-regex-pattern";
2
2
  import { getMjstInstanceOf, getMjstPrimitive } from "@amritk/helpers/mjst-extension";
3
3
  import { multipleOfPassExpr } from "@amritk/helpers/multiple-of-check";
4
4
  import { resolveRef } from "@amritk/helpers/resolve-ref";
5
- import { safeAccessor } from "@amritk/helpers/safe-accessor";
5
+ import { hasOwnCheck, safeAccessor } from "@amritk/helpers/safe-accessor";
6
6
  import { hasAdditionalProperties, hasAllOf, hasAnyOf, hasConst, hasEnum, hasExclusiveMaximum, hasExclusiveMinimum, hasItems, hasMaxItems, hasMaximum, hasMaxLength, hasMaxProperties, hasMinItems, hasMinimum, hasMinLength, hasMinProperties, hasMultipleOf, hasOneOf, hasPattern, hasProperties, hasRef, hasRequired, hasType, hasUniqueItems, isSchemaObject } from "@amritk/helpers/schema-guards";
7
7
  import { findDiscriminator } from "../helpers/find-discriminator.js";
8
8
  import { getDefaultValue } from "../helpers/get-default-value.js";
@@ -46,6 +46,11 @@ const getPrefixItems = (schema) => {
46
46
  return prefix;
47
47
  return Array.isArray(schema.items) ? schema.items : null;
48
48
  };
49
+ const everyTailItem = (accessor, itemCheck, schema) => {
50
+ const prefix = isSchemaObject(schema) ? schema["prefixItems"] : void 0;
51
+ const tailStart = Array.isArray(prefix) ? prefix.length : 0;
52
+ return tailStart === 0 ? `${accessor}.every((_it) => ${itemCheck})` : `${accessor}.every((_it, _ix) => _ix < ${tailStart} || (${itemCheck}))`;
53
+ };
49
54
  const prefixItemsCapsLength = (schema) => {
50
55
  if (!isSchemaObject(schema))
51
56
  return false;
@@ -206,7 +211,7 @@ const generateValidationExpression = (key, schema, defaultValue, isRequired, roo
206
211
  case "string": {
207
212
  checks.push(`typeof ${accessor} === "string"`);
208
213
  if (hasPattern(schema)) {
209
- checks.push(`/${escapeRegexPattern(schema.pattern)}/.test(${accessor})`);
214
+ checks.push(`${regexLiteral(schema.pattern)}.test(${accessor})`);
210
215
  }
211
216
  if (hasMinLength(schema)) {
212
217
  checks.push(`${accessor}.length >= ${schema.minLength}`);
@@ -262,7 +267,7 @@ const generateValidationExpression = (key, schema, defaultValue, isRequired, roo
262
267
  checks.push(`isObject(${accessor})`);
263
268
  if (hasRequired(schema) && schema.required.length > 0) {
264
269
  for (const requiredKey of schema.required) {
265
- checks.push(`${JSON.stringify(requiredKey)} in ${accessor}`);
270
+ checks.push(hasOwnCheck(accessor, requiredKey));
266
271
  }
267
272
  }
268
273
  if (hasMinProperties(schema)) {
@@ -325,6 +330,7 @@ const generatePrefixItemsMap = (accessor, prefix, capLength, rootSchema, visited
325
330
  return capLength ? `${mapped}.slice(0, ${prefix.length})` : mapped;
326
331
  };
327
332
  export {
333
+ everyTailItem,
328
334
  generatePrefixItemsMap,
329
335
  generateValidationExpression,
330
336
  getPrefixItems,
@@ -1,4 +1,15 @@
1
1
  import type { JSONSchema } from 'json-schema-typed/draft-2020-12';
2
+ /**
3
+ * What the matcher needs beyond the schema node itself: the root document (to
4
+ * resolve `$ref`), the recursion depth (which seeds collision-free loop variable
5
+ * names and caps expression growth), and the `$ref` pointers already open on this
6
+ * path (a cycle cannot be inlined, so it bails rather than recursing forever).
7
+ */
8
+ export type MatchContext = {
9
+ readonly rootSchema?: Record<string, unknown>;
10
+ readonly depth?: number;
11
+ readonly seen?: ReadonlySet<string>;
12
+ };
2
13
  /**
3
14
  * Builds a boolean expression that is `true` *exactly* when `accessor` matches
4
15
  * `schema` — sound in **both** directions (true ⇒ valid, false ⇒ invalid) — or
@@ -17,13 +28,21 @@ import type { JSONSchema } from 'json-schema-typed/draft-2020-12';
17
28
  * types), so a type-less schema guards each family by its runtime type rather
18
29
  * than ignoring it — the subtlety `propertyNames: { maxLength: 3 }` depends on.
19
30
  *
20
- * `depth` seeds the `.every` element variable so nested array matches never
21
- * shadow one another.
31
+ * `ctx.depth` seeds the loop variable names so nested walks never shadow one
32
+ * another, and caps how far the expression may grow.
33
+ */
34
+ export declare const subschemaMatchExpr: (accessor: string, schema: JSONSchema, ctx?: MatchContext) => string | null;
35
+ /**
36
+ * True when a `$ref` node carries a keyword that constrains the value on its own.
37
+ * 2020-12 applies a `$ref`'s siblings alongside the target, so delegating the
38
+ * whole node to the referenced parser (which knows nothing about them) drops
39
+ * every one of them — and a fast path built from the target's shape validator
40
+ * skips them too.
22
41
  */
23
- export declare const subschemaMatchExpr: (accessor: string, schema: JSONSchema, depth?: number) => string | null;
42
+ export declare const hasConstrainingRefSibling: (schema: JSONSchema) => boolean;
24
43
  /**
25
44
  * Whether {@link subschemaMatchExpr} can build an exact matcher for `schema`.
26
45
  * The generation-time guard uses this to decide whether a `contains`,
27
46
  * `propertyNames`, or `dependentSchemas` subschema is enforceable.
28
47
  */
29
- export declare const canMatchSubschema: (schema: JSONSchema) => boolean;
48
+ export declare const canMatchSubschema: (schema: JSONSchema, rootSchema?: Record<string, unknown>) => boolean;