@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.
- package/AI.md +6 -1
- package/README.md +25 -2
- package/dist/core/document.d.ts +25 -0
- package/dist/core/document.js +17 -2
- package/dist/core/filter.d.ts +6 -0
- package/dist/core/filter.js +162 -42
- package/dist/core/index.d.ts +2 -2
- package/dist/core/index.js +2 -1
- package/dist/core/jsonpath.d.ts +23 -9
- package/dist/core/jsonpath.js +303 -59
- package/dist/core/lint.d.ts +19 -3
- package/dist/core/lint.js +26 -11
- package/dist/core/ruleset.js +1 -0
- package/dist/core/runner.js +8 -6
- package/dist/core/types.d.ts +12 -0
- package/dist/core/validate-ruleset.js +3 -0
- package/dist/index.d.ts +8 -4
- package/dist/index.js +2 -1
- package/dist/parsers/depth.js +18 -14
- package/dist/parsers/index.d.ts +1 -0
- package/dist/parsers/index.js +2 -0
- package/dist/parsers/json-locator.d.ts +19 -0
- package/dist/parsers/json-locator.js +151 -0
- package/dist/parsers/json.d.ts +8 -1
- package/dist/parsers/json.js +34 -21
- package/package.json +2 -1
package/dist/core/jsonpath.js
CHANGED
|
@@ -128,7 +128,13 @@ const bracketSelector = (content, onError) => {
|
|
|
128
128
|
onError(filter.error);
|
|
129
129
|
return { kind: "none" };
|
|
130
130
|
}
|
|
131
|
-
return {
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
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 (
|
|
483
|
-
const
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
const
|
|
488
|
-
out[
|
|
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
|
|
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
|
-
|
|
498
|
-
|
|
499
|
-
if (
|
|
500
|
-
|
|
501
|
-
|
|
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
|
-
|
|
504
|
-
|
|
505
|
-
|
|
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
|
-
|
|
508
|
-
if (selector.index !== void 0)
|
|
509
|
-
numericChild.push(r);
|
|
678
|
+
stillPending.push([d, step + 1]);
|
|
510
679
|
}
|
|
511
|
-
const
|
|
512
|
-
|
|
513
|
-
|
|
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
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
const
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
if (
|
|
527
|
-
|
|
528
|
-
|
|
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
|
-
|
|
531
|
-
const
|
|
532
|
-
|
|
533
|
-
|
|
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,
|
package/dist/core/lint.d.ts
CHANGED
|
@@ -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,
|
|
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:
|
|
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:
|
|
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
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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(
|
|
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,
|
package/dist/core/ruleset.js
CHANGED
package/dist/core/runner.js
CHANGED
|
@@ -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
|
-
|
|
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(
|
|
89
|
-
order.push(
|
|
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
|
|
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];
|
package/dist/core/types.d.ts
CHANGED
|
@@ -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:
|
|
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:
|
|
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). */
|