@amritk/lint 0.7.1 → 0.8.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.
@@ -128,7 +128,13 @@ const bracketSelector = (content, onError) => {
128
128
  onError(filter.error);
129
129
  return { kind: "none" };
130
130
  }
131
- return { kind: "filter", test: filter.test, source: expr, usesPath: filter.usesPath };
131
+ return {
132
+ kind: "filter",
133
+ test: filter.test,
134
+ source: expr,
135
+ usesPath: filter.usesPath,
136
+ objectsOnly: filter.objectsOnly
137
+ };
132
138
  }
133
139
  if (trimmed.startsWith("(") && trimmed.endsWith(")")) {
134
140
  const inner = trimmed.slice(1, -1).trim();
@@ -410,31 +416,87 @@ const applySelector = (node, selector, root, out) => {
410
416
  return;
411
417
  }
412
418
  };
413
- const walkDescendants = (node, visit) => {
419
+ const DATA_KEYWORDS = /* @__PURE__ */ new Set(["example", "examples", "default", "const", "enum"]);
420
+ const NAME_MAP_KEYWORDS = /* @__PURE__ */ new Set([
421
+ "properties",
422
+ "patternProperties",
423
+ "$defs",
424
+ "definitions",
425
+ "dependentSchemas",
426
+ "schemas",
427
+ "responses",
428
+ "parameters",
429
+ "headers",
430
+ "requestBodies",
431
+ "securitySchemes",
432
+ "links",
433
+ "callbacks",
434
+ "pathItems",
435
+ "webhooks",
436
+ "encoding",
437
+ "variables",
438
+ "dependencies",
439
+ "messages",
440
+ "channels",
441
+ "operations",
442
+ "servers",
443
+ "serverVariables",
444
+ "messageTraits",
445
+ "operationTraits",
446
+ "correlationIds",
447
+ "replies",
448
+ "replyAddresses",
449
+ "serverBindings",
450
+ "channelBindings",
451
+ "operationBindings",
452
+ "messageBindings"
453
+ ]);
454
+ const isNameMap = (node) => {
455
+ const chain = [];
456
+ for (let n = node; n !== void 0 && n.parent !== void 0; n = n.parent)
457
+ chain.push(n);
458
+ let nameMap = false;
459
+ for (let i = chain.length - 1; i >= 0; i--) {
460
+ const key = chain[i].key;
461
+ nameMap = !nameMap && typeof key === "string" && NAME_MAP_KEYWORDS.has(key);
462
+ }
463
+ return nameMap;
464
+ };
465
+ const isDataChild = (node) => typeof node.key === "string" && DATA_KEYWORDS.has(node.key) && isObject(node.parent?.value) && !isNameMap(node.parent);
466
+ const walkDescendants = (node, visit, schemaAware) => {
414
467
  const stack = [node];
468
+ const nameMaps = [schemaAware && isNameMap(node)];
415
469
  while (stack.length > 0) {
416
470
  const current = stack.pop();
471
+ const nameMap = nameMaps.pop();
417
472
  visit(current);
418
473
  const value = current.value;
419
474
  if (Array.isArray(value)) {
420
- for (let idx = value.length - 1; idx >= 0; idx--)
475
+ for (let idx = value.length - 1; idx >= 0; idx--) {
421
476
  stack.push({ value: value[idx], parent: current, key: idx });
477
+ nameMaps.push(false);
478
+ }
422
479
  } else if (isObject(value)) {
423
480
  const keys = Object.keys(value);
424
481
  for (let k = keys.length - 1; k >= 0; k--) {
425
482
  const key = keys[k];
483
+ if (schemaAware && !nameMap && DATA_KEYWORDS.has(key))
484
+ continue;
426
485
  stack.push({ value: value[key], parent: current, key });
486
+ nameMaps.push(schemaAware && !nameMap && NAME_MAP_KEYWORDS.has(key));
427
487
  }
428
488
  }
429
489
  }
430
490
  };
431
- const applySteps = (root, initial, steps) => {
491
+ const applySteps = (root, initial, steps, schemaAware = false) => {
432
492
  let current = initial;
433
493
  for (const step of steps) {
434
- const next = [];
494
+ let next = [];
435
495
  if (step.recursive) {
436
496
  for (const node of current)
437
- walkDescendants(node, (n) => applySelector(n, step.selector, root, next));
497
+ walkDescendants(node, (n) => applySelector(n, step.selector, root, next), schemaAware);
498
+ if (schemaAware)
499
+ next = next.filter((n) => !isDataChild(n));
438
500
  } else {
439
501
  for (const node of current)
440
502
  applySelector(node, step.selector, root, next);
@@ -443,7 +505,7 @@ const applySteps = (root, initial, steps) => {
443
505
  }
444
506
  return current;
445
507
  };
446
- const runSteps = (root, steps) => applySteps(root, [{ value: root, parent: void 0, key: void 0 }], steps);
508
+ const runSteps = (root, steps, schemaAware = false) => applySteps(root, [{ value: root, parent: void 0, key: void 0 }], steps, schemaAware);
447
509
  const toMatches = (nodes) => {
448
510
  const out = new Array(nodes.length);
449
511
  for (let i = 0; i < nodes.length; i++) {
@@ -452,92 +514,274 @@ const toMatches = (nodes) => {
452
514
  }
453
515
  return out;
454
516
  };
455
- const queryCompiled = (data, compiled) => {
517
+ const queryCompiled = (data, compiled, options = {}) => {
456
518
  if (compiled.error !== void 0)
457
519
  return [];
458
520
  if (data === null || data === void 0)
459
521
  return [];
460
- return toMatches(runSteps(data, compiled.steps));
522
+ return toMatches(runSteps(data, compiled.steps, options.schemaAware === true));
461
523
  };
462
- const queryMany = (data, compiled) => {
524
+ const queryMany = (data, compiled, options = []) => {
463
525
  const out = new Array(compiled.length);
464
526
  if (data === null || data === void 0) {
465
527
  for (let i = 0; i < compiled.length; i++)
466
528
  out[i] = [];
467
529
  return out;
468
530
  }
469
- const recursive = [];
531
+ const shared = [];
532
+ const descents = [];
470
533
  for (let i = 0; i < compiled.length; i++) {
471
534
  const c = compiled[i];
472
535
  if (c.error !== void 0) {
473
536
  out[i] = [];
474
537
  continue;
475
538
  }
476
- const first = c.steps[0];
477
- if (first?.recursive)
478
- recursive.push(i);
479
- else
480
- out[i] = queryCompiled(data, c);
539
+ const schemaAware = options[i]?.schemaAware === true;
540
+ const descent = c.hasDescent ? toDescent(c.steps, schemaAware) : void 0;
541
+ if (descent === void 0) {
542
+ out[i] = queryCompiled(data, c, { schemaAware });
543
+ continue;
544
+ }
545
+ shared.push(i);
546
+ descents.push(descent);
481
547
  }
482
- if (recursive.length > 0) {
483
- const firsts = recursive.map((i) => compiled[i].steps[0]);
484
- const seeds = recursive.map(() => []);
485
- seedDescents(data, firsts, seeds);
486
- for (let r = 0; r < recursive.length; r++) {
487
- const c = compiled[recursive[r]];
488
- out[recursive[r]] = toMatches(applySteps(data, seeds[r], c.steps.slice(1)));
548
+ if (descents.length > 0) {
549
+ const seeds = descents.map(() => []);
550
+ walkShared(data, descents, seeds);
551
+ for (let d = 0; d < descents.length; d++) {
552
+ const { rest, schemaAware } = descents[d];
553
+ const seeded = seeds[d];
554
+ out[shared[d]] = toMatches(rest.length === 0 ? seeded : applySteps(data, seeded, rest, schemaAware));
489
555
  }
490
556
  }
491
557
  return out;
492
558
  };
493
- const seedDescents = (data, firsts, seeds) => {
559
+ const childPathString = (parent, key) => {
560
+ const path = pathOf(parent);
561
+ path.push(key);
562
+ return pathToJsonPathString(path);
563
+ };
564
+ const toDescent = (steps, schemaAware) => {
565
+ const prefix = [];
566
+ for (let i = 0; i < steps.length; i++) {
567
+ const step = steps[i];
568
+ if (step.recursive)
569
+ return { prefix, first: step.selector, rest: steps.slice(i + 1), schemaAware };
570
+ const kind = step.selector.kind;
571
+ if (kind !== "child" && kind !== "index" && kind !== "wildcard" && kind !== "filter")
572
+ return void 0;
573
+ prefix.push(step.selector);
574
+ }
575
+ return void 0;
576
+ };
577
+ const prefixSelects = (selector, parent, key, child, root) => {
578
+ const value = parent.value;
579
+ switch (selector.kind) {
580
+ case "child":
581
+ return typeof key === "number" ? selector.index === key : selector.name === key;
582
+ case "index": {
583
+ if (typeof key === "number") {
584
+ const idx = selector.index < 0 ? value.length + selector.index : selector.index;
585
+ return idx === key;
586
+ }
587
+ return key === String(selector.index);
588
+ }
589
+ case "wildcard":
590
+ return true;
591
+ case "filter":
592
+ return selector.test(child, key, value, root, selector.usesPath ? childPathString(parent, key) : "", parent.key);
593
+ default:
594
+ return false;
595
+ }
596
+ };
597
+ const createScope = (descents, active, pending) => {
494
598
  const byName = /* @__PURE__ */ new Map();
495
599
  const numericChild = [];
600
+ const wildcards = [];
601
+ const filters = [];
496
602
  const others = [];
497
- for (let r = 0; r < firsts.length; r++) {
498
- const selector = firsts[r].selector;
499
- if (selector.kind !== "child") {
500
- others.push(r);
501
- continue;
603
+ let schemaAware = false;
604
+ for (const d of active) {
605
+ if (descents[d].schemaAware)
606
+ schemaAware = true;
607
+ const selector = descents[d].first;
608
+ if (selector.kind === "child") {
609
+ const existing = byName.get(selector.name);
610
+ if (existing === void 0)
611
+ byName.set(selector.name, [d]);
612
+ else
613
+ existing.push(d);
614
+ if (selector.index !== void 0)
615
+ numericChild.push(d);
616
+ } else if (selector.kind === "wildcard")
617
+ wildcards.push(d);
618
+ else if (selector.kind === "filter")
619
+ filters.push(d);
620
+ else
621
+ others.push(d);
622
+ }
623
+ return {
624
+ active,
625
+ pending,
626
+ byName,
627
+ numericChild,
628
+ wildcards,
629
+ filters,
630
+ others,
631
+ settled: void 0,
632
+ next: /* @__PURE__ */ new Map(),
633
+ schemaAware,
634
+ outsideData: void 0
635
+ };
636
+ };
637
+ const outsideDataScope = (descents, scope) => {
638
+ if (scope.outsideData === void 0) {
639
+ scope.outsideData = createScope(descents, scope.active.filter((d) => !descents[d].schemaAware), scope.pending);
640
+ }
641
+ return scope.outsideData;
642
+ };
643
+ const settledScope = (descents, scope) => {
644
+ if (scope.pending.length === 0)
645
+ return scope;
646
+ if (scope.settled === void 0)
647
+ scope.settled = createScope(descents, scope.active, []);
648
+ return scope.settled;
649
+ };
650
+ const childScope = (descents, scope, parent, key, child, root) => {
651
+ const pending = scope.pending;
652
+ if (pending.length === 0)
653
+ return scope;
654
+ let advanced;
655
+ for (const entry of pending) {
656
+ const [d, step] = entry;
657
+ const prefix = descents[d].prefix;
658
+ if (prefixSelects(prefix[step], parent, key, child, root)) {
659
+ if (advanced === void 0)
660
+ advanced = [];
661
+ advanced.push(entry);
502
662
  }
503
- const existing = byName.get(selector.name);
504
- if (existing === void 0)
505
- byName.set(selector.name, [r]);
663
+ }
664
+ if (advanced === void 0)
665
+ return settledScope(descents, scope);
666
+ let signature = "";
667
+ for (const [d, step] of advanced)
668
+ signature += `${d}:${step},`;
669
+ const cached = scope.next.get(signature);
670
+ if (cached !== void 0)
671
+ return cached;
672
+ const active = scope.active.slice();
673
+ const stillPending = [];
674
+ for (const [d, step] of advanced) {
675
+ if (step + 1 === descents[d].prefix.length)
676
+ active.push(d);
506
677
  else
507
- existing.push(r);
508
- if (selector.index !== void 0)
509
- numericChild.push(r);
678
+ stillPending.push([d, step + 1]);
510
679
  }
511
- const stack = [{ value: data, parent: void 0, key: void 0 }];
512
- while (stack.length > 0) {
513
- const current = stack.pop();
680
+ const built = createScope(descents, active, stillPending);
681
+ scope.next.set(signature, built);
682
+ return built;
683
+ };
684
+ const isLive = (scope) => scope.active.length > 0 || scope.pending.length > 0;
685
+ const walkShared = (data, descents, seeds) => {
686
+ const rootActive = [];
687
+ const rootPending = [];
688
+ for (let d = 0; d < descents.length; d++) {
689
+ if (descents[d].prefix.length === 0)
690
+ rootActive.push(d);
691
+ else
692
+ rootPending.push([d, 0]);
693
+ }
694
+ const nodes = [{ value: data, parent: void 0, key: void 0 }];
695
+ const scopes = [createScope(descents, rootActive, rootPending)];
696
+ const tracksNameMaps = descents.some((descent) => descent.schemaAware);
697
+ const nameMaps = [false];
698
+ while (nodes.length > 0) {
699
+ const current = nodes.pop();
700
+ const outer = scopes.pop();
701
+ const nameMap = tracksNameMaps ? nameMaps.pop() : false;
514
702
  const value = current.value;
515
- if (Array.isArray(value)) {
516
- for (let idx = value.length - 1; idx >= 0; idx--)
517
- stack.push({ value: value[idx], parent: current, key: idx });
518
- for (const r of numericChild)
519
- applySelector(current, firsts[r].selector, data, seeds[r]);
520
- } else if (isObject(value)) {
521
- const base = stack.length;
522
- for (const key of Object.keys(value)) {
523
- const child = { value: value[key], parent: current, key };
524
- stack.push(child);
525
- const interested = byName.get(key);
526
- if (interested !== void 0)
527
- for (const r of interested)
528
- seeds[r].push(child);
703
+ const base = nodes.length;
704
+ if (typeof value === "object" && value !== null) {
705
+ const isArray = Array.isArray(value);
706
+ const keys = isArray ? void 0 : Object.keys(value);
707
+ const length = isArray ? value.length : keys.length;
708
+ for (let i = 0; i < length; i++) {
709
+ const key = isArray ? i : keys[i];
710
+ const child = value[key];
711
+ const scope = outer.schemaAware && !isArray && !nameMap && DATA_KEYWORDS.has(key) ? outsideDataScope(descents, outer) : outer;
712
+ const { byName, wildcards, filters } = scope;
713
+ let node;
714
+ if (!isArray) {
715
+ const interested = byName.get(key);
716
+ if (interested !== void 0) {
717
+ node = { value: child, parent: current, key };
718
+ for (const d of interested)
719
+ seeds[d].push(node);
720
+ }
721
+ }
722
+ if (wildcards.length > 0) {
723
+ if (node === void 0)
724
+ node = { value: child, parent: current, key };
725
+ for (const d of wildcards)
726
+ seeds[d].push(node);
727
+ }
728
+ if (filters.length > 0) {
729
+ const childIsObject = isObject(child);
730
+ for (const d of filters) {
731
+ const selector = descents[d].first;
732
+ if (selector.objectsOnly && !childIsObject)
733
+ continue;
734
+ const path = selector.usesPath ? childPathString(current, key) : "";
735
+ if (selector.test(child, key, value, data, path, current.key)) {
736
+ if (node === void 0)
737
+ node = { value: child, parent: current, key };
738
+ seeds[d].push(node);
739
+ }
740
+ }
741
+ }
742
+ const inner = childScope(descents, scope, current, key, child, data);
743
+ const visit = typeof child === "object" && child !== null ? isLive(inner) : inner.others.length > 0;
744
+ if (visit) {
745
+ nodes.push(node === void 0 ? { value: child, parent: current, key } : node);
746
+ scopes.push(inner);
747
+ if (tracksNameMaps)
748
+ nameMaps.push(!isArray && !nameMap && NAME_MAP_KEYWORDS.has(key));
749
+ }
529
750
  }
530
- for (let i = base, j = stack.length - 1; i < j; i++, j--) {
531
- const held = stack[i];
532
- stack[i] = stack[j];
533
- stack[j] = held;
751
+ if (isArray) {
752
+ for (const d of outer.numericChild) {
753
+ applySelector(current, descents[d].first, data, seeds[d]);
754
+ }
755
+ }
756
+ for (let i = base, j = nodes.length - 1; i < j; i++, j--) {
757
+ const heldNode = nodes[i];
758
+ nodes[i] = nodes[j];
759
+ nodes[j] = heldNode;
760
+ const heldScope = scopes[i];
761
+ scopes[i] = scopes[j];
762
+ scopes[j] = heldScope;
763
+ if (tracksNameMaps) {
764
+ const heldNameMap = nameMaps[i];
765
+ nameMaps[i] = nameMaps[j];
766
+ nameMaps[j] = heldNameMap;
767
+ }
768
+ }
769
+ }
770
+ for (const d of outer.others) {
771
+ const descent = descents[d];
772
+ if (!descent.schemaAware) {
773
+ applySelector(current, descent.first, data, seeds[d]);
774
+ continue;
534
775
  }
776
+ const found = [];
777
+ applySelector(current, descent.first, data, found);
778
+ for (const node of found)
779
+ if (!isDataChild(node))
780
+ seeds[d].push(node);
535
781
  }
536
- for (const r of others)
537
- applySelector(current, firsts[r].selector, data, seeds[r]);
538
782
  }
539
783
  };
540
- const query = (data, expression) => queryCompiled(data, compileQuery(expression));
784
+ const query = (data, expression, options = {}) => queryCompiled(data, compileQuery(expression), options);
541
785
  export {
542
786
  compileQuery,
543
787
  query,
@@ -28,9 +28,20 @@ export type LintResolverResult = {
28
28
  * `@amritk/resolve-refs` keeps the `$ref` node in place when a chain would close
29
29
  * a cycle, which is what makes its output safe here.
30
30
  */
31
- export type LintResolver = (document: Document, context: {
31
+ export type LintResolver = (document: Document,
32
+ /** `input` is the document text: `''` when the caller linted data without it. */
33
+ context: {
32
34
  input: string;
33
35
  }) => LintResolverResult | Promise<LintResolverResult>;
36
+ /**
37
+ * What {@link lint} reads: the document text, or data the caller has already
38
+ * parsed — optionally with the text it came from, which is only read to put
39
+ * line:column ranges on findings. See {@link createDocumentFromData}.
40
+ */
41
+ export type LintInput = string | {
42
+ data: unknown;
43
+ text?: string;
44
+ };
34
45
  /** Options for {@link lint} / {@link lintWithResult}. */
35
46
  export type LintOptions = IDocumentOptions & {
36
47
  /** A normalized ruleset to evaluate (built via {@link createRuleset}). */
@@ -65,7 +76,12 @@ export type LintResult = {
65
76
  * level entry point (such as `@amritk/lint`) supplies the ruleset, and
66
77
  * optionally a resolver and format-skip predicate; {@link lint} is a thin
67
78
  * wrapper returning just the diagnostics.
79
+ *
80
+ * Handed `{ data, text? }` instead of a string, the parse is skipped: the
81
+ * run starts from the caller's data, and `text` (when given) is consulted
82
+ * only to put ranges on findings. The resolver then sees `text` (or `''`)
83
+ * as its `input`.
68
84
  */
69
- export declare const lintWithResult: (input: string, options: LintOptions) => Promise<LintResult>;
85
+ export declare const lintWithResult: (input: LintInput, options: LintOptions) => Promise<LintResult>;
70
86
  /** Lints `input` against a normalized `ruleset` and returns just the findings. */
71
- export declare const lint: (input: string, options: LintOptions) => Promise<IDiagnostic[]>;
87
+ export declare const lint: (input: LintInput, options: LintOptions) => Promise<IDiagnostic[]>;
package/dist/core/lint.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { detectFormat } from "../parsers/index.js";
2
- import { createDocument } from "./document.js";
2
+ import { createDocument, createDocumentFromData } from "./document.js";
3
3
  import { bySourceThenPosition, withoutDuplicates } from "./order.js";
4
4
  import { runPlugins } from "./plugin.js";
5
5
  import { createLinter } from "./runner.js";
@@ -13,20 +13,35 @@ const toParserSeverity = (value) => {
13
13
  };
14
14
  const lintWithResult = async (input, options) => {
15
15
  const { ruleset, resolve, skip, plugins, ...documentOptions } = options;
16
- const duplicateKeys = toParserSeverity(ruleset.parserOptions?.duplicateKeys);
17
- const incompatibleValues = toParserSeverity(ruleset.parserOptions?.incompatibleValues);
18
- const document = createDocument(input, {
19
- ...documentOptions,
20
- ...duplicateKeys !== void 0 ? { duplicateKeys } : {},
21
- ...incompatibleValues !== void 0 ? { incompatibleValues } : {}
22
- });
16
+ let document;
17
+ let text;
18
+ if (typeof input === "string") {
19
+ const duplicateKeys = toParserSeverity(ruleset.parserOptions?.duplicateKeys);
20
+ const incompatibleValues = toParserSeverity(ruleset.parserOptions?.incompatibleValues);
21
+ document = createDocument(input, {
22
+ ...documentOptions,
23
+ ...duplicateKeys !== void 0 ? { duplicateKeys } : {},
24
+ ...incompatibleValues !== void 0 ? { incompatibleValues } : {}
25
+ });
26
+ text = input;
27
+ } else {
28
+ if (plugins && plugins.length > 0 && input.text === void 0) {
29
+ throw new Error("Plugins need the document text: pass `text` alongside `data`");
30
+ }
31
+ document = createDocumentFromData(input.data, {
32
+ ...documentOptions.source !== void 0 ? { source: documentOptions.source } : {},
33
+ ...documentOptions.format !== void 0 ? { format: documentOptions.format } : {},
34
+ ...input.text !== void 0 ? { text: input.text } : {}
35
+ });
36
+ text = input.text ?? "";
37
+ }
23
38
  if (skip?.(document.data))
24
39
  return { diagnostics: [], pluginData: {} };
25
40
  let resolved = document.data;
26
41
  let sources;
27
42
  let resolverDiagnostics = [];
28
43
  if (resolve) {
29
- const result = await resolve(document, { input });
44
+ const result = await resolve(document, { input: text });
30
45
  resolved = result.resolved;
31
46
  sources = result.sources;
32
47
  if (result.diagnostics)
@@ -48,9 +63,9 @@ const lintWithResult = async (input, options) => {
48
63
  const diagnostics = withoutDuplicates([...parserResults, ...resolverDiagnostics, ...ruleResults].sort(bySourceThenPosition));
49
64
  if (!plugins || plugins.length === 0)
50
65
  return { diagnostics, pluginData: {} };
51
- const format = documentOptions.format ?? detectFormat(input);
66
+ const format = documentOptions.format ?? detectFormat(text);
52
67
  const run = runPlugins(plugins, diagnostics, {
53
- input,
68
+ input: text,
54
69
  format,
55
70
  document,
56
71
  resolved,
@@ -67,6 +67,7 @@ const normalizeRule = (name, def, modifier, declaring = {}) => {
67
67
  formats: formats ? new Set(formats) : void 0,
68
68
  recommended,
69
69
  resolved: def.resolved ?? true,
70
+ ...def.descent === "schema" ? { descent: "schema" } : {},
70
71
  documentationUrl
71
72
  };
72
73
  };
@@ -81,19 +81,21 @@ const runPlan = async (ruleset, rules, data, document, formats, out, sources) =>
81
81
  out.push(errorDiagnostic(rule, [], `Rule "${rule.name}" references unknown function "${missing}"`, ZERO_RANGE, document.source));
82
82
  continue;
83
83
  }
84
+ const schemaAware = rule.descent === "schema";
84
85
  for (const given of ruleset.expandGiven(rule.given, formats)) {
85
- let group = groups.get(given);
86
+ const key = schemaAware ? `${given}\0schema` : given;
87
+ let group = groups.get(key);
86
88
  if (!group) {
87
- group = { compiled: compileQuery(given), rules: [] };
88
- groups.set(given, group);
89
- order.push(given);
89
+ group = { compiled: compileQuery(given), schemaAware, rules: [] };
90
+ groups.set(key, group);
91
+ order.push(key);
90
92
  }
91
93
  if (group.rules[group.rules.length - 1] !== rule)
92
94
  group.rules.push(rule);
93
95
  }
94
96
  }
95
- const compiled = order.map((given) => groups.get(given).compiled);
96
- const matchesPerGiven = queryMany(data, compiled);
97
+ const planned = order.map((key) => groups.get(key));
98
+ const matchesPerGiven = queryMany(data, planned.map((group) => group.compiled), planned.map((group) => group.schemaAware ? { schemaAware: true } : void 0));
97
99
  for (let i = 0; i < order.length; i++) {
98
100
  const group = groups.get(order[i]);
99
101
  const matches = matchesPerGiven[i];
@@ -63,6 +63,16 @@ export type IRuleDefinition = {
63
63
  recommended?: boolean;
64
64
  /** When false, the rule runs against the unresolved document. Default true. */
65
65
  resolved?: boolean;
66
+ /**
67
+ * How `..` in `given` reads the document. `'all'` (the default) visits every
68
+ * node. `'schema'` treats it as JSON Schema — or an OpenAPI/AsyncAPI document
69
+ * — and stays out of instance data: the values of `example`, `examples`,
70
+ * `default`, `const` and `enum` are neither visited nor matched, except
71
+ * where those words are names in a map such as `properties`. Use it for
72
+ * rules about schemas, so an example payload that reads `"type": "object"`
73
+ * is not mistaken for one.
74
+ */
75
+ descent?: 'all' | 'schema';
66
76
  documentationUrl?: string;
67
77
  };
68
78
  /** Shorthand a rule may take in `rules`: a definition, a boolean, or a severity. */
@@ -124,6 +134,8 @@ export type ResolvedRule = {
124
134
  formats?: Set<string> | undefined;
125
135
  recommended: boolean;
126
136
  resolved: boolean;
137
+ /** See {@link IRuleDefinition.descent}; absent means `'all'`. */
138
+ descent?: 'all' | 'schema' | undefined;
127
139
  documentationUrl?: string | undefined;
128
140
  };
129
141
  /** A single finding emitted by the runner, mapped back to a source range. */
@@ -57,6 +57,9 @@ const validateRule = (name, entry, path, problems) => {
57
57
  } else {
58
58
  validateThen(entry["then"], [...path, "then"], problems);
59
59
  }
60
+ if (entry["descent"] !== void 0 && entry["descent"] !== "all" && entry["descent"] !== "schema") {
61
+ problems.push({ message: `Rule "${name}" \`descent\` must be "all" or "schema"`, path: [...path, "descent"] });
62
+ }
60
63
  if (entry["severity"] !== void 0 && !isValidSeverity(entry["severity"])) {
61
64
  problems.push({ message: `Rule "${name}" has invalid severity`, path: [...path, "severity"] });
62
65
  }
package/dist/index.d.ts CHANGED
@@ -1,8 +1,8 @@
1
- import { type IDocumentOptions, type LintPlugin, type LintResolver, type ResolvedExtend, type Ruleset } from './core/index.js';
1
+ import { type IDocumentOptions, type LintInput, type LintPlugin, type LintResolver, type ResolvedExtend, type Ruleset } from './core/index.js';
2
2
  import type { IDiagnostic, RulesetDefinition } from './core/types.js';
3
3
  import { type AppliedFix, type FixerRegistry } from './fix/index.js';
4
4
  import { type IRulesetTrustOptions } from './ruleset-files.js';
5
- export { type AliasDefinition, type CompiledPath, compileQuery, createDocument, createLinter, type Document, detectFormats, type ExtendModifier, type ExtendResolver, type Format, globToRegExp, type IDocumentOptions, type IQueryMatch, type IRulesetProblem, type IRunOptions, type Linter, type LintOptions, type LintPlugin, type LintPluginContext, type LintPluginResult, type LintResolver, type LintResolverResult, type LintResult, lint, lintWithResult, matchesGlob, type PluginRunResult, pointerToPath, query, queryCompiled, queryMany, type ResolvedExtend, type Ruleset, type RulesetOptions, resolveSourceOrigin, resolveSourceOriginFromMap, resolveSourcePath, runPlugins, validateRuleset, } from './core/index.js';
5
+ export { type AliasDefinition, type CompiledPath, compileQuery, createDocument, createDocumentFromData, createLinter, type Document, detectFormats, type ExtendModifier, type ExtendResolver, type Format, globToRegExp, type IDataDocumentOptions, type IDocumentOptions, type IQueryMatch, type IRulesetProblem, type IRunOptions, type Linter, type LintInput, type LintOptions, type LintPlugin, type LintPluginContext, type LintPluginResult, type LintResolver, type LintResolverResult, type LintResult, lint, lintWithResult, matchesGlob, type PluginRunResult, pointerToPath, query, queryCompiled, queryMany, type ResolvedExtend, type Ruleset, type RulesetOptions, resolveSourceOrigin, resolveSourceOriginFromMap, resolveSourcePath, runPlugins, validateRuleset, } from './core/index.js';
6
6
  export { type AppliedFix, type ApplyFixesOptions, applyFixes, createFixPlugin, type EditOp, FIX_PLUGIN_NAME, type FixContext, type Fixer, type FixerRegistry, type FixPluginData, type FixResult, type ParserFormat, } from './fix/index.js';
7
7
  export { alphabetical, builtinFunctions, type CasingType, casing, defined, enumeration, falsy, type IAlphabeticalOptions, type ICasingOptions, type IOrOptions, type ISchemaOptions, type IUnreferencedReusableObjectOptions, type IXorOptions, length, or, pattern, schema, truthy, typedEnum, undefinedFn, unreferencedReusableObject, xor, } from './functions/index.js';
8
8
  export { detectFormat, parseWithPointers } from './parsers/index.js';
@@ -87,6 +87,10 @@ export type ILintResult = {
87
87
  * ruleset. Returns just the findings; use {@link lintDocumentWithResult} for the
88
88
  * full result.
89
89
  *
90
+ * A caller that has already parsed the document passes `{ data, text? }`
91
+ * instead of the string, and the parse is skipped: `text` is only read to
92
+ * put line:column ranges on the findings (see `createDocumentFromData`).
93
+ *
90
94
  * @example
91
95
  * ```ts
92
96
  * const ruleset = {
@@ -99,13 +103,13 @@ export type ILintResult = {
99
103
  * // findings[].severity is numeric (0 error, 1 warn, 2 info, 3 hint).
100
104
  * ```
101
105
  */
102
- export declare const lintDocument: (input: string, options?: ILintOptions) => Promise<IDiagnostic[]>;
106
+ export declare const lintDocument: (input: LintInput, options?: ILintOptions) => Promise<IDiagnostic[]>;
103
107
  /**
104
108
  * Like {@link lintDocument}, but returns the full {@link ILintResult} — including
105
109
  * anything the configured `plugins` produced (e.g. the auto-fix plugin's
106
110
  * rewritten `output`).
107
111
  */
108
- export declare const lintDocumentWithResult: (input: string, options?: ILintOptions) => Promise<ILintResult>;
112
+ export declare const lintDocumentWithResult: (input: LintInput, options?: ILintOptions) => Promise<ILintResult>;
109
113
  /** Options for {@link fixDocument}: the lint options plus auto-fix controls. */
110
114
  export type IFixOptions = Omit<ILintOptions, 'plugins'> & {
111
115
  /** The fixers to apply, keyed by rule code. Defaults to an empty registry (no fixes). */