@formspec/build 0.1.0-alpha.13 → 0.1.0-alpha.15
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/README.md +20 -20
- package/dist/__tests__/alias-chain-propagation.test.d.ts +9 -0
- package/dist/__tests__/alias-chain-propagation.test.d.ts.map +1 -0
- package/dist/__tests__/extension-runtime.integration.test.d.ts +2 -0
- package/dist/__tests__/extension-runtime.integration.test.d.ts.map +1 -0
- package/dist/__tests__/fixtures/alias-chains.d.ts +37 -0
- package/dist/__tests__/fixtures/alias-chains.d.ts.map +1 -0
- package/dist/__tests__/fixtures/edge-cases.d.ts +11 -0
- package/dist/__tests__/fixtures/edge-cases.d.ts.map +1 -1
- package/dist/__tests__/fixtures/example-a-builtins.d.ts +13 -13
- package/dist/__tests__/fixtures/example-interface-types.d.ts +33 -33
- package/dist/__tests__/fixtures/example-interface-types.d.ts.map +1 -1
- package/dist/__tests__/jsdoc-constraints.test.d.ts +4 -5
- package/dist/__tests__/jsdoc-constraints.test.d.ts.map +1 -1
- package/dist/__tests__/json-utils.test.d.ts +5 -0
- package/dist/__tests__/json-utils.test.d.ts.map +1 -0
- package/dist/__tests__/parity/fixtures/plan-status/chain-dsl.d.ts +19 -0
- package/dist/__tests__/parity/fixtures/plan-status/chain-dsl.d.ts.map +1 -0
- package/dist/__tests__/parity/fixtures/plan-status/expected-ir.d.ts +6 -0
- package/dist/__tests__/parity/fixtures/plan-status/expected-ir.d.ts.map +1 -0
- package/dist/__tests__/parity/fixtures/plan-status/tsdoc.d.ts +17 -0
- package/dist/__tests__/parity/fixtures/plan-status/tsdoc.d.ts.map +1 -0
- package/dist/__tests__/parity/fixtures/usd-cents/chain-dsl.d.ts +9 -0
- package/dist/__tests__/parity/fixtures/usd-cents/chain-dsl.d.ts.map +1 -0
- package/dist/__tests__/parity/fixtures/usd-cents/expected-ir.d.ts +6 -0
- package/dist/__tests__/parity/fixtures/usd-cents/expected-ir.d.ts.map +1 -0
- package/dist/__tests__/parity/fixtures/usd-cents/tsdoc.d.ts +19 -0
- package/dist/__tests__/parity/fixtures/usd-cents/tsdoc.d.ts.map +1 -0
- package/dist/__tests__/parity/utils.d.ts +6 -1
- package/dist/__tests__/parity/utils.d.ts.map +1 -1
- package/dist/__tests__/path-target-parser.test.d.ts +9 -0
- package/dist/__tests__/path-target-parser.test.d.ts.map +1 -0
- package/dist/analyzer/class-analyzer.d.ts +1 -1
- package/dist/analyzer/class-analyzer.d.ts.map +1 -1
- package/dist/analyzer/jsdoc-constraints.d.ts +8 -52
- package/dist/analyzer/jsdoc-constraints.d.ts.map +1 -1
- package/dist/analyzer/json-utils.d.ts +22 -0
- package/dist/analyzer/json-utils.d.ts.map +1 -0
- package/dist/analyzer/tsdoc-parser.d.ts +24 -12
- package/dist/analyzer/tsdoc-parser.d.ts.map +1 -1
- package/dist/browser.cjs +452 -94
- package/dist/browser.cjs.map +1 -1
- package/dist/browser.d.ts +15 -2
- package/dist/browser.d.ts.map +1 -1
- package/dist/browser.js +450 -94
- package/dist/browser.js.map +1 -1
- package/dist/build.d.ts +132 -5
- package/dist/canonicalize/tsdoc-canonicalizer.d.ts +3 -3
- package/dist/cli.cjs +406 -104
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +407 -104
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +386 -102
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +20 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +386 -104
- package/dist/index.js.map +1 -1
- package/dist/internals.cjs +597 -172
- package/dist/internals.cjs.map +1 -1
- package/dist/internals.js +597 -172
- package/dist/internals.js.map +1 -1
- package/dist/json-schema/generator.d.ts +8 -2
- package/dist/json-schema/generator.d.ts.map +1 -1
- package/dist/json-schema/ir-generator.d.ts +25 -2
- package/dist/json-schema/ir-generator.d.ts.map +1 -1
- package/dist/json-schema/types.d.ts +1 -1
- package/dist/json-schema/types.d.ts.map +1 -1
- package/dist/validate/constraint-validator.d.ts +3 -7
- package/dist/validate/constraint-validator.d.ts.map +1 -1
- package/package.json +3 -3
package/dist/internals.js
CHANGED
|
@@ -177,7 +177,7 @@ function canonicalizeArrayField(field) {
|
|
|
177
177
|
const itemsType = {
|
|
178
178
|
kind: "object",
|
|
179
179
|
properties: itemProperties,
|
|
180
|
-
additionalProperties:
|
|
180
|
+
additionalProperties: true
|
|
181
181
|
};
|
|
182
182
|
const type = { kind: "array", items: itemsType };
|
|
183
183
|
const constraints = [];
|
|
@@ -212,7 +212,7 @@ function canonicalizeObjectField(field) {
|
|
|
212
212
|
const type = {
|
|
213
213
|
kind: "object",
|
|
214
214
|
properties,
|
|
215
|
-
additionalProperties:
|
|
215
|
+
additionalProperties: true
|
|
216
216
|
};
|
|
217
217
|
return buildFieldNode(field.name, type, field.required, buildAnnotations(field.label));
|
|
218
218
|
}
|
|
@@ -460,9 +460,6 @@ import * as ts4 from "typescript";
|
|
|
460
460
|
|
|
461
461
|
// src/analyzer/jsdoc-constraints.ts
|
|
462
462
|
import * as ts3 from "typescript";
|
|
463
|
-
import {
|
|
464
|
-
BUILTIN_CONSTRAINT_DEFINITIONS as BUILTIN_CONSTRAINT_DEFINITIONS2
|
|
465
|
-
} from "@formspec/core";
|
|
466
463
|
|
|
467
464
|
// src/analyzer/tsdoc-parser.ts
|
|
468
465
|
import * as ts2 from "typescript";
|
|
@@ -476,22 +473,35 @@ import {
|
|
|
476
473
|
TextRange
|
|
477
474
|
} from "@microsoft/tsdoc";
|
|
478
475
|
import {
|
|
479
|
-
BUILTIN_CONSTRAINT_DEFINITIONS
|
|
476
|
+
BUILTIN_CONSTRAINT_DEFINITIONS,
|
|
477
|
+
normalizeConstraintTagName,
|
|
478
|
+
isBuiltinConstraintName
|
|
480
479
|
} from "@formspec/core";
|
|
480
|
+
|
|
481
|
+
// src/analyzer/json-utils.ts
|
|
482
|
+
function tryParseJson(text) {
|
|
483
|
+
try {
|
|
484
|
+
return JSON.parse(text);
|
|
485
|
+
} catch {
|
|
486
|
+
return null;
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
// src/analyzer/tsdoc-parser.ts
|
|
481
491
|
var NUMERIC_CONSTRAINT_MAP = {
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
492
|
+
minimum: "minimum",
|
|
493
|
+
maximum: "maximum",
|
|
494
|
+
exclusiveMinimum: "exclusiveMinimum",
|
|
495
|
+
exclusiveMaximum: "exclusiveMaximum",
|
|
496
|
+
multipleOf: "multipleOf"
|
|
486
497
|
};
|
|
487
498
|
var LENGTH_CONSTRAINT_MAP = {
|
|
488
|
-
|
|
489
|
-
|
|
499
|
+
minLength: "minLength",
|
|
500
|
+
maxLength: "maxLength",
|
|
501
|
+
minItems: "minItems",
|
|
502
|
+
maxItems: "maxItems"
|
|
490
503
|
};
|
|
491
|
-
var TAGS_REQUIRING_RAW_TEXT = /* @__PURE__ */ new Set(["
|
|
492
|
-
function isBuiltinConstraintName(tagName) {
|
|
493
|
-
return tagName in BUILTIN_CONSTRAINT_DEFINITIONS;
|
|
494
|
-
}
|
|
504
|
+
var TAGS_REQUIRING_RAW_TEXT = /* @__PURE__ */ new Set(["pattern", "enumOptions"]);
|
|
495
505
|
function createFormSpecTSDocConfig() {
|
|
496
506
|
const config = new TSDocConfiguration();
|
|
497
507
|
for (const tagName of Object.keys(BUILTIN_CONSTRAINT_DEFINITIONS)) {
|
|
@@ -503,6 +513,15 @@ function createFormSpecTSDocConfig() {
|
|
|
503
513
|
})
|
|
504
514
|
);
|
|
505
515
|
}
|
|
516
|
+
for (const tagName of ["displayName", "description"]) {
|
|
517
|
+
config.addTagDefinition(
|
|
518
|
+
new TSDocTagDefinition({
|
|
519
|
+
tagName: "@" + tagName,
|
|
520
|
+
syntaxKind: TSDocTagSyntaxKind.BlockTag,
|
|
521
|
+
allowMultiple: true
|
|
522
|
+
})
|
|
523
|
+
);
|
|
524
|
+
}
|
|
506
525
|
return config;
|
|
507
526
|
}
|
|
508
527
|
var sharedParser;
|
|
@@ -531,7 +550,28 @@ function parseTSDocTags(node, file = "") {
|
|
|
531
550
|
);
|
|
532
551
|
const docComment = parserContext.docComment;
|
|
533
552
|
for (const block of docComment.customBlocks) {
|
|
534
|
-
const tagName = block.blockTag.tagName.substring(1);
|
|
553
|
+
const tagName = normalizeConstraintTagName(block.blockTag.tagName.substring(1));
|
|
554
|
+
if (tagName === "displayName" || tagName === "description") {
|
|
555
|
+
const text2 = extractBlockText(block).trim();
|
|
556
|
+
if (text2 === "") continue;
|
|
557
|
+
const provenance2 = provenanceForComment(range, sourceFile, file, tagName);
|
|
558
|
+
if (tagName === "displayName") {
|
|
559
|
+
annotations.push({
|
|
560
|
+
kind: "annotation",
|
|
561
|
+
annotationKind: "displayName",
|
|
562
|
+
value: text2,
|
|
563
|
+
provenance: provenance2
|
|
564
|
+
});
|
|
565
|
+
} else {
|
|
566
|
+
annotations.push({
|
|
567
|
+
kind: "annotation",
|
|
568
|
+
annotationKind: "description",
|
|
569
|
+
value: text2,
|
|
570
|
+
provenance: provenance2
|
|
571
|
+
});
|
|
572
|
+
}
|
|
573
|
+
continue;
|
|
574
|
+
}
|
|
535
575
|
if (TAGS_REQUIRING_RAW_TEXT.has(tagName)) continue;
|
|
536
576
|
const text = extractBlockText(block).trim();
|
|
537
577
|
if (text === "") continue;
|
|
@@ -552,7 +592,7 @@ function parseTSDocTags(node, file = "") {
|
|
|
552
592
|
}
|
|
553
593
|
const jsDocTagsAll = ts2.getJSDocTags(node);
|
|
554
594
|
for (const tag of jsDocTagsAll) {
|
|
555
|
-
const tagName = tag.tagName.text;
|
|
595
|
+
const tagName = normalizeConstraintTagName(tag.tagName.text);
|
|
556
596
|
if (!TAGS_REQUIRING_RAW_TEXT.has(tagName)) continue;
|
|
557
597
|
const commentText = getTagCommentText(tag);
|
|
558
598
|
if (commentText === void 0 || commentText.trim() === "") continue;
|
|
@@ -563,43 +603,17 @@ function parseTSDocTags(node, file = "") {
|
|
|
563
603
|
constraints.push(constraintNode);
|
|
564
604
|
}
|
|
565
605
|
}
|
|
566
|
-
let displayName;
|
|
567
|
-
let description;
|
|
568
|
-
let displayNameTag;
|
|
569
|
-
let descriptionTag;
|
|
570
|
-
for (const tag of jsDocTagsAll) {
|
|
571
|
-
const tagName = tag.tagName.text;
|
|
572
|
-
const commentText = getTagCommentText(tag);
|
|
573
|
-
if (commentText === void 0 || commentText.trim() === "") {
|
|
574
|
-
continue;
|
|
575
|
-
}
|
|
576
|
-
const trimmed = commentText.trim();
|
|
577
|
-
if (tagName === "Field_displayName") {
|
|
578
|
-
displayName = trimmed;
|
|
579
|
-
displayNameTag = tag;
|
|
580
|
-
} else if (tagName === "Field_description") {
|
|
581
|
-
description = trimmed;
|
|
582
|
-
descriptionTag = tag;
|
|
583
|
-
}
|
|
584
|
-
}
|
|
585
|
-
if (displayName !== void 0 && displayNameTag) {
|
|
586
|
-
annotations.push({
|
|
587
|
-
kind: "annotation",
|
|
588
|
-
annotationKind: "displayName",
|
|
589
|
-
value: displayName,
|
|
590
|
-
provenance: provenanceForJSDocTag(displayNameTag, file)
|
|
591
|
-
});
|
|
592
|
-
}
|
|
593
|
-
if (description !== void 0 && descriptionTag) {
|
|
594
|
-
annotations.push({
|
|
595
|
-
kind: "annotation",
|
|
596
|
-
annotationKind: "description",
|
|
597
|
-
value: description,
|
|
598
|
-
provenance: provenanceForJSDocTag(descriptionTag, file)
|
|
599
|
-
});
|
|
600
|
-
}
|
|
601
606
|
return { constraints, annotations };
|
|
602
607
|
}
|
|
608
|
+
function extractPathTarget(text) {
|
|
609
|
+
const trimmed = text.trimStart();
|
|
610
|
+
const match = /^:([a-zA-Z_]\w*)\s+([\s\S]*)$/.exec(trimmed);
|
|
611
|
+
if (!match?.[1] || !match[2]) return null;
|
|
612
|
+
return {
|
|
613
|
+
path: { segments: [match[1]] },
|
|
614
|
+
remainingText: match[2]
|
|
615
|
+
};
|
|
616
|
+
}
|
|
603
617
|
function extractBlockText(block) {
|
|
604
618
|
return extractPlainText(block.content);
|
|
605
619
|
}
|
|
@@ -622,9 +636,12 @@ function parseConstraintValue(tagName, text, provenance) {
|
|
|
622
636
|
if (!isBuiltinConstraintName(tagName)) {
|
|
623
637
|
return null;
|
|
624
638
|
}
|
|
639
|
+
const pathResult = extractPathTarget(text);
|
|
640
|
+
const effectiveText = pathResult ? pathResult.remainingText : text;
|
|
641
|
+
const path2 = pathResult?.path;
|
|
625
642
|
const expectedType = BUILTIN_CONSTRAINT_DEFINITIONS[tagName];
|
|
626
643
|
if (expectedType === "number") {
|
|
627
|
-
const value = Number(
|
|
644
|
+
const value = Number(effectiveText);
|
|
628
645
|
if (Number.isNaN(value)) {
|
|
629
646
|
return null;
|
|
630
647
|
}
|
|
@@ -634,6 +651,7 @@ function parseConstraintValue(tagName, text, provenance) {
|
|
|
634
651
|
kind: "constraint",
|
|
635
652
|
constraintKind: numericKind,
|
|
636
653
|
value,
|
|
654
|
+
...path2 && { path: path2 },
|
|
637
655
|
provenance
|
|
638
656
|
};
|
|
639
657
|
}
|
|
@@ -643,42 +661,41 @@ function parseConstraintValue(tagName, text, provenance) {
|
|
|
643
661
|
kind: "constraint",
|
|
644
662
|
constraintKind: lengthKind,
|
|
645
663
|
value,
|
|
664
|
+
...path2 && { path: path2 },
|
|
646
665
|
provenance
|
|
647
666
|
};
|
|
648
667
|
}
|
|
649
668
|
return null;
|
|
650
669
|
}
|
|
651
670
|
if (expectedType === "json") {
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
members.push(id);
|
|
665
|
-
}
|
|
671
|
+
const parsed = tryParseJson(effectiveText);
|
|
672
|
+
if (!Array.isArray(parsed)) {
|
|
673
|
+
return null;
|
|
674
|
+
}
|
|
675
|
+
const members = [];
|
|
676
|
+
for (const item of parsed) {
|
|
677
|
+
if (typeof item === "string" || typeof item === "number") {
|
|
678
|
+
members.push(item);
|
|
679
|
+
} else if (typeof item === "object" && item !== null && "id" in item) {
|
|
680
|
+
const id = item["id"];
|
|
681
|
+
if (typeof id === "string" || typeof id === "number") {
|
|
682
|
+
members.push(id);
|
|
666
683
|
}
|
|
667
684
|
}
|
|
668
|
-
return {
|
|
669
|
-
kind: "constraint",
|
|
670
|
-
constraintKind: "allowedMembers",
|
|
671
|
-
members,
|
|
672
|
-
provenance
|
|
673
|
-
};
|
|
674
|
-
} catch {
|
|
675
|
-
return null;
|
|
676
685
|
}
|
|
686
|
+
return {
|
|
687
|
+
kind: "constraint",
|
|
688
|
+
constraintKind: "allowedMembers",
|
|
689
|
+
members,
|
|
690
|
+
...path2 && { path: path2 },
|
|
691
|
+
provenance
|
|
692
|
+
};
|
|
677
693
|
}
|
|
678
694
|
return {
|
|
679
695
|
kind: "constraint",
|
|
680
696
|
constraintKind: "pattern",
|
|
681
|
-
pattern:
|
|
697
|
+
pattern: effectiveText,
|
|
698
|
+
...path2 && { path: path2 },
|
|
682
699
|
provenance
|
|
683
700
|
};
|
|
684
701
|
}
|
|
@@ -850,18 +867,19 @@ function analyzeFieldToIR(prop, checker, file, typeRegistry, visiting) {
|
|
|
850
867
|
const tsType = checker.getTypeAtLocation(prop);
|
|
851
868
|
const optional = prop.questionToken !== void 0;
|
|
852
869
|
const provenance = provenanceForNode(prop, file);
|
|
853
|
-
|
|
870
|
+
let type = resolveTypeNode(tsType, checker, file, typeRegistry, visiting);
|
|
854
871
|
const constraints = [];
|
|
855
872
|
if (prop.type) {
|
|
856
873
|
constraints.push(...extractTypeAliasConstraintNodes(prop.type, checker, file));
|
|
857
874
|
}
|
|
858
875
|
constraints.push(...extractJSDocConstraintNodes(prop, file));
|
|
859
|
-
|
|
876
|
+
let annotations = [];
|
|
860
877
|
annotations.push(...extractJSDocAnnotationNodes(prop, file));
|
|
861
878
|
const defaultAnnotation = extractDefaultValueAnnotation(prop.initializer, file);
|
|
862
879
|
if (defaultAnnotation) {
|
|
863
880
|
annotations.push(defaultAnnotation);
|
|
864
881
|
}
|
|
882
|
+
({ type, annotations } = applyEnumMemberDisplayNames(type, annotations));
|
|
865
883
|
return {
|
|
866
884
|
kind: "field",
|
|
867
885
|
name,
|
|
@@ -880,14 +898,15 @@ function analyzeInterfacePropertyToIR(prop, checker, file, typeRegistry, visitin
|
|
|
880
898
|
const tsType = checker.getTypeAtLocation(prop);
|
|
881
899
|
const optional = prop.questionToken !== void 0;
|
|
882
900
|
const provenance = provenanceForNode(prop, file);
|
|
883
|
-
|
|
901
|
+
let type = resolveTypeNode(tsType, checker, file, typeRegistry, visiting);
|
|
884
902
|
const constraints = [];
|
|
885
903
|
if (prop.type) {
|
|
886
904
|
constraints.push(...extractTypeAliasConstraintNodes(prop.type, checker, file));
|
|
887
905
|
}
|
|
888
906
|
constraints.push(...extractJSDocConstraintNodes(prop, file));
|
|
889
|
-
|
|
907
|
+
let annotations = [];
|
|
890
908
|
annotations.push(...extractJSDocAnnotationNodes(prop, file));
|
|
909
|
+
({ type, annotations } = applyEnumMemberDisplayNames(type, annotations));
|
|
891
910
|
return {
|
|
892
911
|
kind: "field",
|
|
893
912
|
name,
|
|
@@ -898,6 +917,68 @@ function analyzeInterfacePropertyToIR(prop, checker, file, typeRegistry, visitin
|
|
|
898
917
|
provenance
|
|
899
918
|
};
|
|
900
919
|
}
|
|
920
|
+
function applyEnumMemberDisplayNames(type, annotations) {
|
|
921
|
+
if (!annotations.some(
|
|
922
|
+
(annotation) => annotation.annotationKind === "displayName" && annotation.value.trim().startsWith(":")
|
|
923
|
+
)) {
|
|
924
|
+
return { type, annotations: [...annotations] };
|
|
925
|
+
}
|
|
926
|
+
const consumed = /* @__PURE__ */ new Set();
|
|
927
|
+
const nextType = rewriteEnumDisplayNames(type, annotations, consumed);
|
|
928
|
+
if (consumed.size === 0) {
|
|
929
|
+
return { type, annotations: [...annotations] };
|
|
930
|
+
}
|
|
931
|
+
return {
|
|
932
|
+
type: nextType,
|
|
933
|
+
annotations: annotations.filter((annotation) => !consumed.has(annotation))
|
|
934
|
+
};
|
|
935
|
+
}
|
|
936
|
+
function rewriteEnumDisplayNames(type, annotations, consumed) {
|
|
937
|
+
switch (type.kind) {
|
|
938
|
+
case "enum":
|
|
939
|
+
return applyEnumMemberDisplayNamesToEnum(type, annotations, consumed);
|
|
940
|
+
case "union": {
|
|
941
|
+
return {
|
|
942
|
+
...type,
|
|
943
|
+
members: type.members.map(
|
|
944
|
+
(member) => rewriteEnumDisplayNames(member, annotations, consumed)
|
|
945
|
+
)
|
|
946
|
+
};
|
|
947
|
+
}
|
|
948
|
+
default:
|
|
949
|
+
return type;
|
|
950
|
+
}
|
|
951
|
+
}
|
|
952
|
+
function applyEnumMemberDisplayNamesToEnum(type, annotations, consumed) {
|
|
953
|
+
const displayNames = /* @__PURE__ */ new Map();
|
|
954
|
+
for (const annotation of annotations) {
|
|
955
|
+
if (annotation.annotationKind !== "displayName") continue;
|
|
956
|
+
const parsed = parseEnumMemberDisplayName(annotation.value);
|
|
957
|
+
if (!parsed) continue;
|
|
958
|
+
consumed.add(annotation);
|
|
959
|
+
const member = type.members.find((m) => String(m.value) === parsed.value);
|
|
960
|
+
if (!member) continue;
|
|
961
|
+
displayNames.set(String(member.value), parsed.label);
|
|
962
|
+
}
|
|
963
|
+
if (displayNames.size === 0) {
|
|
964
|
+
return type;
|
|
965
|
+
}
|
|
966
|
+
return {
|
|
967
|
+
...type,
|
|
968
|
+
members: type.members.map((member) => {
|
|
969
|
+
const displayName = displayNames.get(String(member.value));
|
|
970
|
+
return displayName !== void 0 ? { ...member, displayName } : member;
|
|
971
|
+
})
|
|
972
|
+
};
|
|
973
|
+
}
|
|
974
|
+
function parseEnumMemberDisplayName(value) {
|
|
975
|
+
const trimmed = value.trim();
|
|
976
|
+
const match = /^:([^\s]+)\s+([\s\S]+)$/.exec(trimmed);
|
|
977
|
+
if (!match?.[1] || !match[2]) return null;
|
|
978
|
+
const label = match[2].trim();
|
|
979
|
+
if (label === "") return null;
|
|
980
|
+
return { value: match[1], label };
|
|
981
|
+
}
|
|
901
982
|
function resolveTypeNode(type, checker, file, typeRegistry, visiting) {
|
|
902
983
|
if (type.flags & ts4.TypeFlags.String) {
|
|
903
984
|
return { kind: "primitive", primitiveKind: "string" };
|
|
@@ -1008,7 +1089,30 @@ function resolveArrayType(type, checker, file, typeRegistry, visiting) {
|
|
|
1008
1089
|
const items = elementType ? resolveTypeNode(elementType, checker, file, typeRegistry, visiting) : { kind: "primitive", primitiveKind: "string" };
|
|
1009
1090
|
return { kind: "array", items };
|
|
1010
1091
|
}
|
|
1092
|
+
function tryResolveRecordType(type, checker, file, typeRegistry, visiting) {
|
|
1093
|
+
if (type.getProperties().length > 0) {
|
|
1094
|
+
return null;
|
|
1095
|
+
}
|
|
1096
|
+
const indexInfo = checker.getIndexInfoOfType(type, ts4.IndexKind.String);
|
|
1097
|
+
if (!indexInfo) {
|
|
1098
|
+
return null;
|
|
1099
|
+
}
|
|
1100
|
+
if (visiting.has(type)) {
|
|
1101
|
+
return null;
|
|
1102
|
+
}
|
|
1103
|
+
visiting.add(type);
|
|
1104
|
+
try {
|
|
1105
|
+
const valueType = resolveTypeNode(indexInfo.type, checker, file, typeRegistry, visiting);
|
|
1106
|
+
return { kind: "record", valueType };
|
|
1107
|
+
} finally {
|
|
1108
|
+
visiting.delete(type);
|
|
1109
|
+
}
|
|
1110
|
+
}
|
|
1011
1111
|
function resolveObjectType(type, checker, file, typeRegistry, visiting) {
|
|
1112
|
+
const recordNode = tryResolveRecordType(type, checker, file, typeRegistry, visiting);
|
|
1113
|
+
if (recordNode) {
|
|
1114
|
+
return recordNode;
|
|
1115
|
+
}
|
|
1012
1116
|
if (visiting.has(type)) {
|
|
1013
1117
|
return { kind: "object", properties: [], additionalProperties: false };
|
|
1014
1118
|
}
|
|
@@ -1040,7 +1144,7 @@ function resolveObjectType(type, checker, file, typeRegistry, visiting) {
|
|
|
1040
1144
|
const objectNode = {
|
|
1041
1145
|
kind: "object",
|
|
1042
1146
|
properties,
|
|
1043
|
-
additionalProperties:
|
|
1147
|
+
additionalProperties: true
|
|
1044
1148
|
};
|
|
1045
1149
|
if (typeName) {
|
|
1046
1150
|
typeRegistry[typeName] = {
|
|
@@ -1109,14 +1213,23 @@ function buildFieldNodeInfoMap(members, checker, file, typeRegistry, visiting) {
|
|
|
1109
1213
|
}
|
|
1110
1214
|
return map;
|
|
1111
1215
|
}
|
|
1112
|
-
|
|
1216
|
+
var MAX_ALIAS_CHAIN_DEPTH = 8;
|
|
1217
|
+
function extractTypeAliasConstraintNodes(typeNode, checker, file, depth = 0) {
|
|
1113
1218
|
if (!ts4.isTypeReferenceNode(typeNode)) return [];
|
|
1219
|
+
if (depth >= MAX_ALIAS_CHAIN_DEPTH) {
|
|
1220
|
+
const aliasName = typeNode.typeName.getText();
|
|
1221
|
+
throw new Error(
|
|
1222
|
+
`Type alias chain exceeds maximum depth of ${String(MAX_ALIAS_CHAIN_DEPTH)} at alias "${aliasName}" in ${file}. Simplify the alias chain or check for circular references.`
|
|
1223
|
+
);
|
|
1224
|
+
}
|
|
1114
1225
|
const symbol = checker.getSymbolAtLocation(typeNode.typeName);
|
|
1115
1226
|
if (!symbol?.declarations) return [];
|
|
1116
1227
|
const aliasDecl = symbol.declarations.find(ts4.isTypeAliasDeclaration);
|
|
1117
1228
|
if (!aliasDecl) return [];
|
|
1118
1229
|
if (ts4.isTypeLiteralNode(aliasDecl.type)) return [];
|
|
1119
|
-
|
|
1230
|
+
const constraints = extractJSDocConstraintNodes(aliasDecl, file);
|
|
1231
|
+
constraints.push(...extractTypeAliasConstraintNodes(aliasDecl.type, checker, file, depth + 1));
|
|
1232
|
+
return constraints;
|
|
1120
1233
|
}
|
|
1121
1234
|
function provenanceForNode(node, file) {
|
|
1122
1235
|
const sourceFile = node.getSourceFile();
|
|
@@ -1191,11 +1304,21 @@ function detectFormSpecReference(typeNode) {
|
|
|
1191
1304
|
}
|
|
1192
1305
|
|
|
1193
1306
|
// src/json-schema/ir-generator.ts
|
|
1194
|
-
function makeContext() {
|
|
1195
|
-
|
|
1307
|
+
function makeContext(options) {
|
|
1308
|
+
const vendorPrefix = options?.vendorPrefix ?? "x-formspec";
|
|
1309
|
+
if (!vendorPrefix.startsWith("x-")) {
|
|
1310
|
+
throw new Error(
|
|
1311
|
+
`Invalid vendorPrefix "${vendorPrefix}". Extension JSON Schema keywords must start with "x-".`
|
|
1312
|
+
);
|
|
1313
|
+
}
|
|
1314
|
+
return {
|
|
1315
|
+
defs: {},
|
|
1316
|
+
extensionRegistry: options?.extensionRegistry,
|
|
1317
|
+
vendorPrefix
|
|
1318
|
+
};
|
|
1196
1319
|
}
|
|
1197
|
-
function generateJsonSchemaFromIR(ir) {
|
|
1198
|
-
const ctx = makeContext();
|
|
1320
|
+
function generateJsonSchemaFromIR(ir, options) {
|
|
1321
|
+
const ctx = makeContext(options);
|
|
1199
1322
|
for (const [name, typeDef] of Object.entries(ir.typeRegistry)) {
|
|
1200
1323
|
ctx.defs[name] = generateTypeNode(typeDef.type, ctx);
|
|
1201
1324
|
}
|
|
@@ -1238,8 +1361,70 @@ function collectFields(elements, properties, required, ctx) {
|
|
|
1238
1361
|
}
|
|
1239
1362
|
function generateFieldSchema(field, ctx) {
|
|
1240
1363
|
const schema = generateTypeNode(field.type, ctx);
|
|
1241
|
-
|
|
1242
|
-
|
|
1364
|
+
const directConstraints = [];
|
|
1365
|
+
const pathConstraints = [];
|
|
1366
|
+
for (const c of field.constraints) {
|
|
1367
|
+
if (c.path) {
|
|
1368
|
+
pathConstraints.push(c);
|
|
1369
|
+
} else {
|
|
1370
|
+
directConstraints.push(c);
|
|
1371
|
+
}
|
|
1372
|
+
}
|
|
1373
|
+
applyConstraints(schema, directConstraints, ctx);
|
|
1374
|
+
applyAnnotations(schema, field.annotations, ctx);
|
|
1375
|
+
if (pathConstraints.length === 0) {
|
|
1376
|
+
return schema;
|
|
1377
|
+
}
|
|
1378
|
+
return applyPathTargetedConstraints(schema, pathConstraints, ctx);
|
|
1379
|
+
}
|
|
1380
|
+
function applyPathTargetedConstraints(schema, pathConstraints, ctx) {
|
|
1381
|
+
if (schema.type === "array" && schema.items) {
|
|
1382
|
+
schema.items = applyPathTargetedConstraints(schema.items, pathConstraints, ctx);
|
|
1383
|
+
return schema;
|
|
1384
|
+
}
|
|
1385
|
+
const byTarget = /* @__PURE__ */ new Map();
|
|
1386
|
+
for (const c of pathConstraints) {
|
|
1387
|
+
const target = c.path?.segments[0];
|
|
1388
|
+
if (!target) continue;
|
|
1389
|
+
const group = byTarget.get(target) ?? [];
|
|
1390
|
+
group.push(c);
|
|
1391
|
+
byTarget.set(target, group);
|
|
1392
|
+
}
|
|
1393
|
+
const propertyOverrides = {};
|
|
1394
|
+
for (const [target, constraints] of byTarget) {
|
|
1395
|
+
const subSchema = {};
|
|
1396
|
+
applyConstraints(subSchema, constraints, ctx);
|
|
1397
|
+
propertyOverrides[target] = subSchema;
|
|
1398
|
+
}
|
|
1399
|
+
if (schema.$ref) {
|
|
1400
|
+
const { $ref, ...rest } = schema;
|
|
1401
|
+
const refPart = { $ref };
|
|
1402
|
+
const overridePart = {
|
|
1403
|
+
properties: propertyOverrides,
|
|
1404
|
+
...rest
|
|
1405
|
+
};
|
|
1406
|
+
return { allOf: [refPart, overridePart] };
|
|
1407
|
+
}
|
|
1408
|
+
if (schema.type === "object" && schema.properties) {
|
|
1409
|
+
const missingOverrides = {};
|
|
1410
|
+
for (const [target, overrideSchema] of Object.entries(propertyOverrides)) {
|
|
1411
|
+
if (schema.properties[target]) {
|
|
1412
|
+
Object.assign(schema.properties[target], overrideSchema);
|
|
1413
|
+
} else {
|
|
1414
|
+
missingOverrides[target] = overrideSchema;
|
|
1415
|
+
}
|
|
1416
|
+
}
|
|
1417
|
+
if (Object.keys(missingOverrides).length === 0) {
|
|
1418
|
+
return schema;
|
|
1419
|
+
}
|
|
1420
|
+
return {
|
|
1421
|
+
allOf: [schema, { properties: missingOverrides }]
|
|
1422
|
+
};
|
|
1423
|
+
}
|
|
1424
|
+
if (schema.allOf) {
|
|
1425
|
+
schema.allOf = [...schema.allOf, { properties: propertyOverrides }];
|
|
1426
|
+
return schema;
|
|
1427
|
+
}
|
|
1243
1428
|
return schema;
|
|
1244
1429
|
}
|
|
1245
1430
|
function generateTypeNode(type, ctx) {
|
|
@@ -1252,6 +1437,8 @@ function generateTypeNode(type, ctx) {
|
|
|
1252
1437
|
return generateArrayType(type, ctx);
|
|
1253
1438
|
case "object":
|
|
1254
1439
|
return generateObjectType(type, ctx);
|
|
1440
|
+
case "record":
|
|
1441
|
+
return generateRecordType(type, ctx);
|
|
1255
1442
|
case "union":
|
|
1256
1443
|
return generateUnionType(type, ctx);
|
|
1257
1444
|
case "reference":
|
|
@@ -1259,7 +1446,7 @@ function generateTypeNode(type, ctx) {
|
|
|
1259
1446
|
case "dynamic":
|
|
1260
1447
|
return generateDynamicType(type);
|
|
1261
1448
|
case "custom":
|
|
1262
|
-
return generateCustomType(type);
|
|
1449
|
+
return generateCustomType(type, ctx);
|
|
1263
1450
|
default: {
|
|
1264
1451
|
const _exhaustive = type;
|
|
1265
1452
|
return _exhaustive;
|
|
@@ -1308,16 +1495,27 @@ function generateObjectType(type, ctx) {
|
|
|
1308
1495
|
}
|
|
1309
1496
|
return schema;
|
|
1310
1497
|
}
|
|
1498
|
+
function generateRecordType(type, ctx) {
|
|
1499
|
+
return {
|
|
1500
|
+
type: "object",
|
|
1501
|
+
additionalProperties: generateTypeNode(type.valueType, ctx)
|
|
1502
|
+
};
|
|
1503
|
+
}
|
|
1311
1504
|
function generatePropertySchema(prop, ctx) {
|
|
1312
1505
|
const schema = generateTypeNode(prop.type, ctx);
|
|
1313
|
-
applyConstraints(schema, prop.constraints);
|
|
1314
|
-
applyAnnotations(schema, prop.annotations);
|
|
1506
|
+
applyConstraints(schema, prop.constraints, ctx);
|
|
1507
|
+
applyAnnotations(schema, prop.annotations, ctx);
|
|
1315
1508
|
return schema;
|
|
1316
1509
|
}
|
|
1317
1510
|
function generateUnionType(type, ctx) {
|
|
1318
1511
|
if (isBooleanUnion(type)) {
|
|
1319
1512
|
return { type: "boolean" };
|
|
1320
1513
|
}
|
|
1514
|
+
if (isNullableUnion(type)) {
|
|
1515
|
+
return {
|
|
1516
|
+
oneOf: type.members.map((m) => generateTypeNode(m, ctx))
|
|
1517
|
+
};
|
|
1518
|
+
}
|
|
1321
1519
|
return {
|
|
1322
1520
|
anyOf: type.members.map((m) => generateTypeNode(m, ctx))
|
|
1323
1521
|
};
|
|
@@ -1327,6 +1525,13 @@ function isBooleanUnion(type) {
|
|
|
1327
1525
|
const kinds = type.members.map((m) => m.kind);
|
|
1328
1526
|
return kinds.every((k) => k === "primitive") && type.members.every((m) => m.kind === "primitive" && m.primitiveKind === "boolean");
|
|
1329
1527
|
}
|
|
1528
|
+
function isNullableUnion(type) {
|
|
1529
|
+
if (type.members.length !== 2) return false;
|
|
1530
|
+
const nullCount = type.members.filter(
|
|
1531
|
+
(m) => m.kind === "primitive" && m.primitiveKind === "null"
|
|
1532
|
+
).length;
|
|
1533
|
+
return nullCount === 1;
|
|
1534
|
+
}
|
|
1330
1535
|
function generateReferenceType(type) {
|
|
1331
1536
|
return { $ref: `#/$defs/${type.name}` };
|
|
1332
1537
|
}
|
|
@@ -1347,10 +1552,7 @@ function generateDynamicType(type) {
|
|
|
1347
1552
|
"x-formspec-schemaSource": type.sourceKey
|
|
1348
1553
|
};
|
|
1349
1554
|
}
|
|
1350
|
-
function
|
|
1351
|
-
return { type: "object" };
|
|
1352
|
-
}
|
|
1353
|
-
function applyConstraints(schema, constraints) {
|
|
1555
|
+
function applyConstraints(schema, constraints, ctx) {
|
|
1354
1556
|
for (const constraint of constraints) {
|
|
1355
1557
|
switch (constraint.constraintKind) {
|
|
1356
1558
|
case "minimum":
|
|
@@ -1395,6 +1597,7 @@ function applyConstraints(schema, constraints) {
|
|
|
1395
1597
|
case "allowedMembers":
|
|
1396
1598
|
break;
|
|
1397
1599
|
case "custom":
|
|
1600
|
+
applyCustomConstraint(schema, constraint, ctx);
|
|
1398
1601
|
break;
|
|
1399
1602
|
default: {
|
|
1400
1603
|
const _exhaustive = constraint;
|
|
@@ -1403,7 +1606,7 @@ function applyConstraints(schema, constraints) {
|
|
|
1403
1606
|
}
|
|
1404
1607
|
}
|
|
1405
1608
|
}
|
|
1406
|
-
function applyAnnotations(schema, annotations) {
|
|
1609
|
+
function applyAnnotations(schema, annotations, ctx) {
|
|
1407
1610
|
for (const annotation of annotations) {
|
|
1408
1611
|
switch (annotation.annotationKind) {
|
|
1409
1612
|
case "displayName":
|
|
@@ -1423,6 +1626,7 @@ function applyAnnotations(schema, annotations) {
|
|
|
1423
1626
|
case "formatHint":
|
|
1424
1627
|
break;
|
|
1425
1628
|
case "custom":
|
|
1629
|
+
applyCustomAnnotation(schema, annotation, ctx);
|
|
1426
1630
|
break;
|
|
1427
1631
|
default: {
|
|
1428
1632
|
const _exhaustive = annotation;
|
|
@@ -1431,6 +1635,36 @@ function applyAnnotations(schema, annotations) {
|
|
|
1431
1635
|
}
|
|
1432
1636
|
}
|
|
1433
1637
|
}
|
|
1638
|
+
function generateCustomType(type, ctx) {
|
|
1639
|
+
const registration = ctx.extensionRegistry?.findType(type.typeId);
|
|
1640
|
+
if (registration === void 0) {
|
|
1641
|
+
throw new Error(
|
|
1642
|
+
`Cannot generate JSON Schema for custom type "${type.typeId}" without a matching extension registration`
|
|
1643
|
+
);
|
|
1644
|
+
}
|
|
1645
|
+
return registration.toJsonSchema(type.payload, ctx.vendorPrefix);
|
|
1646
|
+
}
|
|
1647
|
+
function applyCustomConstraint(schema, constraint, ctx) {
|
|
1648
|
+
const registration = ctx.extensionRegistry?.findConstraint(constraint.constraintId);
|
|
1649
|
+
if (registration === void 0) {
|
|
1650
|
+
throw new Error(
|
|
1651
|
+
`Cannot generate JSON Schema for custom constraint "${constraint.constraintId}" without a matching extension registration`
|
|
1652
|
+
);
|
|
1653
|
+
}
|
|
1654
|
+
Object.assign(schema, registration.toJsonSchema(constraint.payload, ctx.vendorPrefix));
|
|
1655
|
+
}
|
|
1656
|
+
function applyCustomAnnotation(schema, annotation, ctx) {
|
|
1657
|
+
const registration = ctx.extensionRegistry?.findAnnotation(annotation.annotationId);
|
|
1658
|
+
if (registration === void 0) {
|
|
1659
|
+
throw new Error(
|
|
1660
|
+
`Cannot generate JSON Schema for custom annotation "${annotation.annotationId}" without a matching extension registration`
|
|
1661
|
+
);
|
|
1662
|
+
}
|
|
1663
|
+
if (registration.toJsonSchema === void 0) {
|
|
1664
|
+
return;
|
|
1665
|
+
}
|
|
1666
|
+
Object.assign(schema, registration.toJsonSchema(annotation.value, ctx.vendorPrefix));
|
|
1667
|
+
}
|
|
1434
1668
|
|
|
1435
1669
|
// src/ui-schema/schema.ts
|
|
1436
1670
|
import { z } from "zod";
|
|
@@ -1655,12 +1889,9 @@ function generateClassSchemas(analysis, source) {
|
|
|
1655
1889
|
}
|
|
1656
1890
|
|
|
1657
1891
|
// src/validate/constraint-validator.ts
|
|
1658
|
-
function makeCode(ctx, category, number) {
|
|
1659
|
-
return `${ctx.vendorPrefix}-${category}-${String(number).padStart(3, "0")}`;
|
|
1660
|
-
}
|
|
1661
1892
|
function addContradiction(ctx, message, primary, related) {
|
|
1662
1893
|
ctx.diagnostics.push({
|
|
1663
|
-
code:
|
|
1894
|
+
code: "CONTRADICTING_CONSTRAINTS",
|
|
1664
1895
|
message,
|
|
1665
1896
|
severity: "error",
|
|
1666
1897
|
primaryLocation: primary,
|
|
@@ -1669,7 +1900,7 @@ function addContradiction(ctx, message, primary, related) {
|
|
|
1669
1900
|
}
|
|
1670
1901
|
function addTypeMismatch(ctx, message, primary) {
|
|
1671
1902
|
ctx.diagnostics.push({
|
|
1672
|
-
code:
|
|
1903
|
+
code: "TYPE_MISMATCH",
|
|
1673
1904
|
message,
|
|
1674
1905
|
severity: "error",
|
|
1675
1906
|
primaryLocation: primary,
|
|
@@ -1678,28 +1909,153 @@ function addTypeMismatch(ctx, message, primary) {
|
|
|
1678
1909
|
}
|
|
1679
1910
|
function addUnknownExtension(ctx, message, primary) {
|
|
1680
1911
|
ctx.diagnostics.push({
|
|
1681
|
-
code:
|
|
1912
|
+
code: "UNKNOWN_EXTENSION",
|
|
1682
1913
|
message,
|
|
1683
1914
|
severity: "warning",
|
|
1684
1915
|
primaryLocation: primary,
|
|
1685
1916
|
relatedLocations: []
|
|
1686
1917
|
});
|
|
1687
1918
|
}
|
|
1919
|
+
function addConstraintBroadening(ctx, message, primary, related) {
|
|
1920
|
+
ctx.diagnostics.push({
|
|
1921
|
+
code: "CONSTRAINT_BROADENING",
|
|
1922
|
+
message,
|
|
1923
|
+
severity: "error",
|
|
1924
|
+
primaryLocation: primary,
|
|
1925
|
+
relatedLocations: [related]
|
|
1926
|
+
});
|
|
1927
|
+
}
|
|
1688
1928
|
function findNumeric(constraints, constraintKind) {
|
|
1689
|
-
return constraints.find(
|
|
1690
|
-
(c) => c.constraintKind === constraintKind
|
|
1691
|
-
);
|
|
1929
|
+
return constraints.find((c) => c.constraintKind === constraintKind);
|
|
1692
1930
|
}
|
|
1693
1931
|
function findLength(constraints, constraintKind) {
|
|
1694
|
-
return constraints.find(
|
|
1695
|
-
(c) => c.constraintKind === constraintKind
|
|
1696
|
-
);
|
|
1932
|
+
return constraints.find((c) => c.constraintKind === constraintKind);
|
|
1697
1933
|
}
|
|
1698
1934
|
function findAllowedMembers(constraints) {
|
|
1699
1935
|
return constraints.filter(
|
|
1700
1936
|
(c) => c.constraintKind === "allowedMembers"
|
|
1701
1937
|
);
|
|
1702
1938
|
}
|
|
1939
|
+
function isOrderedBoundConstraint(constraint) {
|
|
1940
|
+
return constraint.constraintKind === "minimum" || constraint.constraintKind === "exclusiveMinimum" || constraint.constraintKind === "minLength" || constraint.constraintKind === "minItems" || constraint.constraintKind === "maximum" || constraint.constraintKind === "exclusiveMaximum" || constraint.constraintKind === "maxLength" || constraint.constraintKind === "maxItems";
|
|
1941
|
+
}
|
|
1942
|
+
function pathKey(constraint) {
|
|
1943
|
+
return constraint.path?.segments.join(".") ?? "";
|
|
1944
|
+
}
|
|
1945
|
+
function orderedBoundFamily(kind) {
|
|
1946
|
+
switch (kind) {
|
|
1947
|
+
case "minimum":
|
|
1948
|
+
case "exclusiveMinimum":
|
|
1949
|
+
return "numeric-lower";
|
|
1950
|
+
case "maximum":
|
|
1951
|
+
case "exclusiveMaximum":
|
|
1952
|
+
return "numeric-upper";
|
|
1953
|
+
case "minLength":
|
|
1954
|
+
return "minLength";
|
|
1955
|
+
case "minItems":
|
|
1956
|
+
return "minItems";
|
|
1957
|
+
case "maxLength":
|
|
1958
|
+
return "maxLength";
|
|
1959
|
+
case "maxItems":
|
|
1960
|
+
return "maxItems";
|
|
1961
|
+
default: {
|
|
1962
|
+
const _exhaustive = kind;
|
|
1963
|
+
return _exhaustive;
|
|
1964
|
+
}
|
|
1965
|
+
}
|
|
1966
|
+
}
|
|
1967
|
+
function isNumericLowerKind(kind) {
|
|
1968
|
+
return kind === "minimum" || kind === "exclusiveMinimum";
|
|
1969
|
+
}
|
|
1970
|
+
function isNumericUpperKind(kind) {
|
|
1971
|
+
return kind === "maximum" || kind === "exclusiveMaximum";
|
|
1972
|
+
}
|
|
1973
|
+
function describeConstraintTag(constraint) {
|
|
1974
|
+
return `@${constraint.constraintKind}`;
|
|
1975
|
+
}
|
|
1976
|
+
function compareConstraintStrength(current, previous) {
|
|
1977
|
+
const family = orderedBoundFamily(current.constraintKind);
|
|
1978
|
+
if (family === "numeric-lower") {
|
|
1979
|
+
if (!isNumericLowerKind(current.constraintKind) || !isNumericLowerKind(previous.constraintKind)) {
|
|
1980
|
+
throw new Error("numeric-lower family received non-numeric lower-bound constraint");
|
|
1981
|
+
}
|
|
1982
|
+
if (current.value !== previous.value) {
|
|
1983
|
+
return current.value > previous.value ? 1 : -1;
|
|
1984
|
+
}
|
|
1985
|
+
if (current.constraintKind === "exclusiveMinimum" && previous.constraintKind === "minimum") {
|
|
1986
|
+
return 1;
|
|
1987
|
+
}
|
|
1988
|
+
if (current.constraintKind === "minimum" && previous.constraintKind === "exclusiveMinimum") {
|
|
1989
|
+
return -1;
|
|
1990
|
+
}
|
|
1991
|
+
return 0;
|
|
1992
|
+
}
|
|
1993
|
+
if (family === "numeric-upper") {
|
|
1994
|
+
if (!isNumericUpperKind(current.constraintKind) || !isNumericUpperKind(previous.constraintKind)) {
|
|
1995
|
+
throw new Error("numeric-upper family received non-numeric upper-bound constraint");
|
|
1996
|
+
}
|
|
1997
|
+
if (current.value !== previous.value) {
|
|
1998
|
+
return current.value < previous.value ? 1 : -1;
|
|
1999
|
+
}
|
|
2000
|
+
if (current.constraintKind === "exclusiveMaximum" && previous.constraintKind === "maximum") {
|
|
2001
|
+
return 1;
|
|
2002
|
+
}
|
|
2003
|
+
if (current.constraintKind === "maximum" && previous.constraintKind === "exclusiveMaximum") {
|
|
2004
|
+
return -1;
|
|
2005
|
+
}
|
|
2006
|
+
return 0;
|
|
2007
|
+
}
|
|
2008
|
+
switch (family) {
|
|
2009
|
+
case "minLength":
|
|
2010
|
+
case "minItems":
|
|
2011
|
+
if (current.value === previous.value) {
|
|
2012
|
+
return 0;
|
|
2013
|
+
}
|
|
2014
|
+
return current.value > previous.value ? 1 : -1;
|
|
2015
|
+
case "maxLength":
|
|
2016
|
+
case "maxItems":
|
|
2017
|
+
if (current.value === previous.value) {
|
|
2018
|
+
return 0;
|
|
2019
|
+
}
|
|
2020
|
+
return current.value < previous.value ? 1 : -1;
|
|
2021
|
+
default: {
|
|
2022
|
+
const _exhaustive = family;
|
|
2023
|
+
return _exhaustive;
|
|
2024
|
+
}
|
|
2025
|
+
}
|
|
2026
|
+
}
|
|
2027
|
+
function checkConstraintBroadening(ctx, fieldName, constraints) {
|
|
2028
|
+
const strongestByKey = /* @__PURE__ */ new Map();
|
|
2029
|
+
for (const constraint of constraints) {
|
|
2030
|
+
if (!isOrderedBoundConstraint(constraint)) {
|
|
2031
|
+
continue;
|
|
2032
|
+
}
|
|
2033
|
+
const key = `${orderedBoundFamily(constraint.constraintKind)}:${pathKey(constraint)}`;
|
|
2034
|
+
const previous = strongestByKey.get(key);
|
|
2035
|
+
if (previous === void 0) {
|
|
2036
|
+
strongestByKey.set(key, constraint);
|
|
2037
|
+
continue;
|
|
2038
|
+
}
|
|
2039
|
+
const strength = compareConstraintStrength(constraint, previous);
|
|
2040
|
+
if (strength < 0) {
|
|
2041
|
+
const displayFieldName = formatPathTargetFieldName(
|
|
2042
|
+
fieldName,
|
|
2043
|
+
constraint.path?.segments ?? []
|
|
2044
|
+
);
|
|
2045
|
+
addConstraintBroadening(
|
|
2046
|
+
ctx,
|
|
2047
|
+
`Field "${displayFieldName}": ${describeConstraintTag(constraint)} (${String(constraint.value)}) is broader than earlier ${describeConstraintTag(previous)} (${String(previous.value)}). Constraints can only narrow.`,
|
|
2048
|
+
constraint.provenance,
|
|
2049
|
+
previous.provenance
|
|
2050
|
+
);
|
|
2051
|
+
continue;
|
|
2052
|
+
}
|
|
2053
|
+
if (strength <= 0) {
|
|
2054
|
+
continue;
|
|
2055
|
+
}
|
|
2056
|
+
strongestByKey.set(key, constraint);
|
|
2057
|
+
}
|
|
2058
|
+
}
|
|
1703
2059
|
function checkNumericContradictions(ctx, fieldName, constraints) {
|
|
1704
2060
|
const min = findNumeric(constraints, "minimum");
|
|
1705
2061
|
const max = findNumeric(constraints, "maximum");
|
|
@@ -1796,6 +2152,8 @@ function typeLabel(type) {
|
|
|
1796
2152
|
return "array";
|
|
1797
2153
|
case "object":
|
|
1798
2154
|
return "object";
|
|
2155
|
+
case "record":
|
|
2156
|
+
return "record";
|
|
1799
2157
|
case "union":
|
|
1800
2158
|
return "union";
|
|
1801
2159
|
case "reference":
|
|
@@ -1810,74 +2168,140 @@ function typeLabel(type) {
|
|
|
1810
2168
|
}
|
|
1811
2169
|
}
|
|
1812
2170
|
}
|
|
1813
|
-
function
|
|
1814
|
-
|
|
1815
|
-
const
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
2171
|
+
function dereferenceType(ctx, type) {
|
|
2172
|
+
let current = type;
|
|
2173
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2174
|
+
while (current.kind === "reference") {
|
|
2175
|
+
if (seen.has(current.name)) {
|
|
2176
|
+
return current;
|
|
2177
|
+
}
|
|
2178
|
+
seen.add(current.name);
|
|
2179
|
+
const definition = ctx.typeRegistry[current.name];
|
|
2180
|
+
if (definition === void 0) {
|
|
2181
|
+
return current;
|
|
2182
|
+
}
|
|
2183
|
+
current = definition.type;
|
|
2184
|
+
}
|
|
2185
|
+
return current;
|
|
2186
|
+
}
|
|
2187
|
+
function resolvePathTargetType(ctx, type, segments) {
|
|
2188
|
+
const effectiveType = dereferenceType(ctx, type);
|
|
2189
|
+
if (segments.length === 0) {
|
|
2190
|
+
return { kind: "resolved", type: effectiveType };
|
|
2191
|
+
}
|
|
2192
|
+
if (effectiveType.kind === "array") {
|
|
2193
|
+
return resolvePathTargetType(ctx, effectiveType.items, segments);
|
|
2194
|
+
}
|
|
2195
|
+
if (effectiveType.kind === "object") {
|
|
2196
|
+
const [segment, ...rest] = segments;
|
|
2197
|
+
if (segment === void 0) {
|
|
2198
|
+
throw new Error("Invariant violation: object path traversal requires a segment");
|
|
2199
|
+
}
|
|
2200
|
+
const property = effectiveType.properties.find((prop) => prop.name === segment);
|
|
2201
|
+
if (property === void 0) {
|
|
2202
|
+
return { kind: "missing-property", segment };
|
|
2203
|
+
}
|
|
2204
|
+
return resolvePathTargetType(ctx, property.type, rest);
|
|
2205
|
+
}
|
|
2206
|
+
return { kind: "unresolvable", type: effectiveType };
|
|
2207
|
+
}
|
|
2208
|
+
function formatPathTargetFieldName(fieldName, path2) {
|
|
2209
|
+
return path2.length === 0 ? fieldName : `${fieldName}.${path2.join(".")}`;
|
|
2210
|
+
}
|
|
2211
|
+
function checkConstraintOnType(ctx, fieldName, type, constraint) {
|
|
2212
|
+
const effectiveType = dereferenceType(ctx, type);
|
|
2213
|
+
const isNumber = effectiveType.kind === "primitive" && effectiveType.primitiveKind === "number";
|
|
2214
|
+
const isString = effectiveType.kind === "primitive" && effectiveType.primitiveKind === "string";
|
|
2215
|
+
const isArray = effectiveType.kind === "array";
|
|
2216
|
+
const isEnum = effectiveType.kind === "enum";
|
|
2217
|
+
const label = typeLabel(effectiveType);
|
|
2218
|
+
const ck = constraint.constraintKind;
|
|
2219
|
+
switch (ck) {
|
|
2220
|
+
case "minimum":
|
|
2221
|
+
case "maximum":
|
|
2222
|
+
case "exclusiveMinimum":
|
|
2223
|
+
case "exclusiveMaximum":
|
|
2224
|
+
case "multipleOf": {
|
|
2225
|
+
if (!isNumber) {
|
|
2226
|
+
addTypeMismatch(
|
|
2227
|
+
ctx,
|
|
2228
|
+
`Field "${fieldName}": constraint "${ck}" is only valid on number fields, but field type is "${label}"`,
|
|
2229
|
+
constraint.provenance
|
|
2230
|
+
);
|
|
1835
2231
|
}
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
2232
|
+
break;
|
|
2233
|
+
}
|
|
2234
|
+
case "minLength":
|
|
2235
|
+
case "maxLength":
|
|
2236
|
+
case "pattern": {
|
|
2237
|
+
if (!isString) {
|
|
2238
|
+
addTypeMismatch(
|
|
2239
|
+
ctx,
|
|
2240
|
+
`Field "${fieldName}": constraint "${ck}" is only valid on string fields, but field type is "${label}"`,
|
|
2241
|
+
constraint.provenance
|
|
2242
|
+
);
|
|
1847
2243
|
}
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
2244
|
+
break;
|
|
2245
|
+
}
|
|
2246
|
+
case "minItems":
|
|
2247
|
+
case "maxItems":
|
|
2248
|
+
case "uniqueItems": {
|
|
2249
|
+
if (!isArray) {
|
|
2250
|
+
addTypeMismatch(
|
|
2251
|
+
ctx,
|
|
2252
|
+
`Field "${fieldName}": constraint "${ck}" is only valid on array fields, but field type is "${label}"`,
|
|
2253
|
+
constraint.provenance
|
|
2254
|
+
);
|
|
1859
2255
|
}
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
2256
|
+
break;
|
|
2257
|
+
}
|
|
2258
|
+
case "allowedMembers": {
|
|
2259
|
+
if (!isEnum) {
|
|
2260
|
+
addTypeMismatch(
|
|
2261
|
+
ctx,
|
|
2262
|
+
`Field "${fieldName}": constraint "allowedMembers" is only valid on enum fields, but field type is "${label}"`,
|
|
2263
|
+
constraint.provenance
|
|
2264
|
+
);
|
|
1869
2265
|
}
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
2266
|
+
break;
|
|
2267
|
+
}
|
|
2268
|
+
case "custom": {
|
|
2269
|
+
checkCustomConstraint(ctx, fieldName, effectiveType, constraint);
|
|
2270
|
+
break;
|
|
2271
|
+
}
|
|
2272
|
+
default: {
|
|
2273
|
+
const _exhaustive = constraint;
|
|
2274
|
+
throw new Error(
|
|
2275
|
+
`Unhandled constraint kind: ${_exhaustive.constraintKind}`
|
|
2276
|
+
);
|
|
2277
|
+
}
|
|
2278
|
+
}
|
|
2279
|
+
}
|
|
2280
|
+
function checkTypeApplicability(ctx, fieldName, type, constraints) {
|
|
2281
|
+
for (const constraint of constraints) {
|
|
2282
|
+
if (constraint.path) {
|
|
2283
|
+
const resolution = resolvePathTargetType(ctx, type, constraint.path.segments);
|
|
2284
|
+
const targetFieldName = formatPathTargetFieldName(fieldName, constraint.path.segments);
|
|
2285
|
+
if (resolution.kind === "missing-property") {
|
|
2286
|
+
addTypeMismatch(
|
|
2287
|
+
ctx,
|
|
2288
|
+
`Field "${fieldName}": path-targeted constraint "${constraint.constraintKind}" references unknown path segment "${resolution.segment}"`,
|
|
2289
|
+
constraint.provenance
|
|
2290
|
+
);
|
|
2291
|
+
continue;
|
|
1873
2292
|
}
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
`
|
|
2293
|
+
if (resolution.kind === "unresolvable") {
|
|
2294
|
+
addTypeMismatch(
|
|
2295
|
+
ctx,
|
|
2296
|
+
`Field "${targetFieldName}": path-targeted constraint "${constraint.constraintKind}" is invalid because type "${typeLabel(resolution.type)}" cannot be traversed`,
|
|
2297
|
+
constraint.provenance
|
|
1878
2298
|
);
|
|
2299
|
+
continue;
|
|
1879
2300
|
}
|
|
2301
|
+
checkConstraintOnType(ctx, targetFieldName, resolution.type, constraint);
|
|
2302
|
+
continue;
|
|
1880
2303
|
}
|
|
2304
|
+
checkConstraintOnType(ctx, fieldName, type, constraint);
|
|
1881
2305
|
}
|
|
1882
2306
|
}
|
|
1883
2307
|
function checkCustomConstraint(ctx, fieldName, type, constraint) {
|
|
@@ -1921,6 +2345,7 @@ function validateConstraints(ctx, name, type, constraints) {
|
|
|
1921
2345
|
checkNumericContradictions(ctx, name, constraints);
|
|
1922
2346
|
checkLengthContradictions(ctx, name, constraints);
|
|
1923
2347
|
checkAllowedMembersContradiction(ctx, name, constraints);
|
|
2348
|
+
checkConstraintBroadening(ctx, name, constraints);
|
|
1924
2349
|
checkTypeApplicability(ctx, name, type, constraints);
|
|
1925
2350
|
}
|
|
1926
2351
|
function validateElement(ctx, element) {
|
|
@@ -1947,8 +2372,8 @@ function validateElement(ctx, element) {
|
|
|
1947
2372
|
function validateIR(ir, options) {
|
|
1948
2373
|
const ctx = {
|
|
1949
2374
|
diagnostics: [],
|
|
1950
|
-
|
|
1951
|
-
|
|
2375
|
+
extensionRegistry: options?.extensionRegistry,
|
|
2376
|
+
typeRegistry: ir.typeRegistry
|
|
1952
2377
|
};
|
|
1953
2378
|
for (const element of ir.elements) {
|
|
1954
2379
|
validateElement(ctx, element);
|