@formspec/build 0.1.0-alpha.14 → 0.1.0-alpha.16
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/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/edge-cases.d.ts +22 -0
- package/dist/__tests__/fixtures/edge-cases.d.ts.map +1 -1
- package/dist/__tests__/fixtures/example-a-builtins.d.ts +6 -6
- package/dist/__tests__/fixtures/example-interface-types.d.ts +26 -26
- package/dist/__tests__/fixtures/example-interface-types.d.ts.map +1 -1
- package/dist/__tests__/fixtures/mixed-authoring-shipping-address.d.ts +30 -0
- package/dist/__tests__/fixtures/mixed-authoring-shipping-address.d.ts.map +1 -0
- package/dist/__tests__/mixed-authoring.test.d.ts +2 -0
- package/dist/__tests__/mixed-authoring.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 +11 -4
- package/dist/__tests__/parity/utils.d.ts.map +1 -1
- package/dist/analyzer/class-analyzer.d.ts +5 -3
- package/dist/analyzer/class-analyzer.d.ts.map +1 -1
- package/dist/analyzer/jsdoc-constraints.d.ts +7 -51
- package/dist/analyzer/jsdoc-constraints.d.ts.map +1 -1
- package/dist/analyzer/tsdoc-parser.d.ts +25 -9
- package/dist/analyzer/tsdoc-parser.d.ts.map +1 -1
- package/dist/browser.cjs +546 -102
- 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 +544 -102
- package/dist/browser.js.map +1 -1
- package/dist/build.d.ts +170 -6
- package/dist/canonicalize/tsdoc-canonicalizer.d.ts +3 -3
- package/dist/canonicalize/tsdoc-canonicalizer.d.ts.map +1 -1
- package/dist/cli.cjs +877 -128
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +876 -131
- package/dist/cli.js.map +1 -1
- package/dist/generators/mixed-authoring.d.ts +45 -0
- package/dist/generators/mixed-authoring.d.ts.map +1 -0
- package/dist/index.cjs +850 -125
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +22 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +847 -129
- package/dist/index.js.map +1 -1
- package/dist/internals.cjs +946 -187
- package/dist/internals.cjs.map +1 -1
- package/dist/internals.js +944 -189
- 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 +27 -4
- 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/ui-schema/ir-generator.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/__tests__/jsdoc-constraints.test.d.ts +0 -10
- package/dist/__tests__/jsdoc-constraints.test.d.ts.map +0 -1
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
|
}
|
|
@@ -325,6 +325,7 @@ function canonicalizeTSDoc(analysis, source) {
|
|
|
325
325
|
irVersion: IR_VERSION2,
|
|
326
326
|
elements,
|
|
327
327
|
typeRegistry: analysis.typeRegistry,
|
|
328
|
+
...analysis.annotations !== void 0 && analysis.annotations.length > 0 && { annotations: analysis.annotations },
|
|
328
329
|
provenance
|
|
329
330
|
};
|
|
330
331
|
}
|
|
@@ -460,11 +461,6 @@ import * as ts4 from "typescript";
|
|
|
460
461
|
|
|
461
462
|
// src/analyzer/jsdoc-constraints.ts
|
|
462
463
|
import * as ts3 from "typescript";
|
|
463
|
-
import {
|
|
464
|
-
BUILTIN_CONSTRAINT_DEFINITIONS as BUILTIN_CONSTRAINT_DEFINITIONS2,
|
|
465
|
-
isBuiltinConstraintName as isBuiltinConstraintName2,
|
|
466
|
-
normalizeConstraintTagName as normalizeConstraintTagName2
|
|
467
|
-
} from "@formspec/core";
|
|
468
464
|
|
|
469
465
|
// src/analyzer/tsdoc-parser.ts
|
|
470
466
|
import * as ts2 from "typescript";
|
|
@@ -506,7 +502,7 @@ var LENGTH_CONSTRAINT_MAP = {
|
|
|
506
502
|
minItems: "minItems",
|
|
507
503
|
maxItems: "maxItems"
|
|
508
504
|
};
|
|
509
|
-
var TAGS_REQUIRING_RAW_TEXT = /* @__PURE__ */ new Set(["pattern", "enumOptions"]);
|
|
505
|
+
var TAGS_REQUIRING_RAW_TEXT = /* @__PURE__ */ new Set(["pattern", "enumOptions", "defaultValue"]);
|
|
510
506
|
function createFormSpecTSDocConfig() {
|
|
511
507
|
const config = new TSDocConfiguration();
|
|
512
508
|
for (const tagName of Object.keys(BUILTIN_CONSTRAINT_DEFINITIONS)) {
|
|
@@ -518,6 +514,15 @@ function createFormSpecTSDocConfig() {
|
|
|
518
514
|
})
|
|
519
515
|
);
|
|
520
516
|
}
|
|
517
|
+
for (const tagName of ["displayName", "description", "format", "placeholder"]) {
|
|
518
|
+
config.addTagDefinition(
|
|
519
|
+
new TSDocTagDefinition({
|
|
520
|
+
tagName: "@" + tagName,
|
|
521
|
+
syntaxKind: TSDocTagSyntaxKind.BlockTag,
|
|
522
|
+
allowMultiple: true
|
|
523
|
+
})
|
|
524
|
+
);
|
|
525
|
+
}
|
|
521
526
|
return config;
|
|
522
527
|
}
|
|
523
528
|
var sharedParser;
|
|
@@ -528,6 +533,12 @@ function getParser() {
|
|
|
528
533
|
function parseTSDocTags(node, file = "") {
|
|
529
534
|
const constraints = [];
|
|
530
535
|
const annotations = [];
|
|
536
|
+
let displayName;
|
|
537
|
+
let description;
|
|
538
|
+
let placeholder;
|
|
539
|
+
let displayNameProvenance;
|
|
540
|
+
let descriptionProvenance;
|
|
541
|
+
let placeholderProvenance;
|
|
531
542
|
const sourceFile = node.getSourceFile();
|
|
532
543
|
const sourceText = sourceFile.getFullText();
|
|
533
544
|
const commentRanges = ts2.getLeadingCommentRanges(sourceText, node.getFullStart());
|
|
@@ -547,9 +558,37 @@ function parseTSDocTags(node, file = "") {
|
|
|
547
558
|
const docComment = parserContext.docComment;
|
|
548
559
|
for (const block of docComment.customBlocks) {
|
|
549
560
|
const tagName = normalizeConstraintTagName(block.blockTag.tagName.substring(1));
|
|
561
|
+
if (tagName === "displayName" || tagName === "description" || tagName === "format" || tagName === "placeholder") {
|
|
562
|
+
const text2 = extractBlockText(block).trim();
|
|
563
|
+
if (text2 === "") continue;
|
|
564
|
+
const provenance2 = provenanceForComment(range, sourceFile, file, tagName);
|
|
565
|
+
if (tagName === "displayName") {
|
|
566
|
+
if (!isMemberTargetDisplayName(text2) && displayName === void 0) {
|
|
567
|
+
displayName = text2;
|
|
568
|
+
displayNameProvenance = provenance2;
|
|
569
|
+
}
|
|
570
|
+
} else if (tagName === "format") {
|
|
571
|
+
annotations.push({
|
|
572
|
+
kind: "annotation",
|
|
573
|
+
annotationKind: "format",
|
|
574
|
+
value: text2,
|
|
575
|
+
provenance: provenance2
|
|
576
|
+
});
|
|
577
|
+
} else {
|
|
578
|
+
if (tagName === "description" && description === void 0) {
|
|
579
|
+
description = text2;
|
|
580
|
+
descriptionProvenance = provenance2;
|
|
581
|
+
} else if (tagName === "placeholder" && placeholder === void 0) {
|
|
582
|
+
placeholder = text2;
|
|
583
|
+
placeholderProvenance = provenance2;
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
continue;
|
|
587
|
+
}
|
|
550
588
|
if (TAGS_REQUIRING_RAW_TEXT.has(tagName)) continue;
|
|
551
589
|
const text = extractBlockText(block).trim();
|
|
552
|
-
|
|
590
|
+
const expectedType = isBuiltinConstraintName(tagName) ? BUILTIN_CONSTRAINT_DEFINITIONS[tagName] : void 0;
|
|
591
|
+
if (text === "" && expectedType !== "boolean") continue;
|
|
553
592
|
const provenance = provenanceForComment(range, sourceFile, file, tagName);
|
|
554
593
|
const constraintNode = parseConstraintValue(tagName, text, provenance);
|
|
555
594
|
if (constraintNode) {
|
|
@@ -557,14 +596,47 @@ function parseTSDocTags(node, file = "") {
|
|
|
557
596
|
}
|
|
558
597
|
}
|
|
559
598
|
if (docComment.deprecatedBlock !== void 0) {
|
|
599
|
+
const message = extractBlockText(docComment.deprecatedBlock).trim();
|
|
560
600
|
annotations.push({
|
|
561
601
|
kind: "annotation",
|
|
562
602
|
annotationKind: "deprecated",
|
|
603
|
+
...message !== "" && { message },
|
|
563
604
|
provenance: provenanceForComment(range, sourceFile, file, "deprecated")
|
|
564
605
|
});
|
|
565
606
|
}
|
|
607
|
+
if (description === void 0 && docComment.remarksBlock !== void 0) {
|
|
608
|
+
const remarks = extractBlockText(docComment.remarksBlock).trim();
|
|
609
|
+
if (remarks !== "") {
|
|
610
|
+
description = remarks;
|
|
611
|
+
descriptionProvenance = provenanceForComment(range, sourceFile, file, "remarks");
|
|
612
|
+
}
|
|
613
|
+
}
|
|
566
614
|
}
|
|
567
615
|
}
|
|
616
|
+
if (displayName !== void 0 && displayNameProvenance !== void 0) {
|
|
617
|
+
annotations.push({
|
|
618
|
+
kind: "annotation",
|
|
619
|
+
annotationKind: "displayName",
|
|
620
|
+
value: displayName,
|
|
621
|
+
provenance: displayNameProvenance
|
|
622
|
+
});
|
|
623
|
+
}
|
|
624
|
+
if (description !== void 0 && descriptionProvenance !== void 0) {
|
|
625
|
+
annotations.push({
|
|
626
|
+
kind: "annotation",
|
|
627
|
+
annotationKind: "description",
|
|
628
|
+
value: description,
|
|
629
|
+
provenance: descriptionProvenance
|
|
630
|
+
});
|
|
631
|
+
}
|
|
632
|
+
if (placeholder !== void 0 && placeholderProvenance !== void 0) {
|
|
633
|
+
annotations.push({
|
|
634
|
+
kind: "annotation",
|
|
635
|
+
annotationKind: "placeholder",
|
|
636
|
+
value: placeholder,
|
|
637
|
+
provenance: placeholderProvenance
|
|
638
|
+
});
|
|
639
|
+
}
|
|
568
640
|
const jsDocTagsAll = ts2.getJSDocTags(node);
|
|
569
641
|
for (const tag of jsDocTagsAll) {
|
|
570
642
|
const tagName = normalizeConstraintTagName(tag.tagName.text);
|
|
@@ -573,47 +645,39 @@ function parseTSDocTags(node, file = "") {
|
|
|
573
645
|
if (commentText === void 0 || commentText.trim() === "") continue;
|
|
574
646
|
const text = commentText.trim();
|
|
575
647
|
const provenance = provenanceForJSDocTag(tag, file);
|
|
648
|
+
if (tagName === "defaultValue") {
|
|
649
|
+
const defaultValueNode = parseDefaultValueValue(text, provenance);
|
|
650
|
+
annotations.push(defaultValueNode);
|
|
651
|
+
continue;
|
|
652
|
+
}
|
|
576
653
|
const constraintNode = parseConstraintValue(tagName, text, provenance);
|
|
577
654
|
if (constraintNode) {
|
|
578
655
|
constraints.push(constraintNode);
|
|
579
656
|
}
|
|
580
657
|
}
|
|
658
|
+
return { constraints, annotations };
|
|
659
|
+
}
|
|
660
|
+
function extractDisplayNameMetadata(node) {
|
|
581
661
|
let displayName;
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
const tagName = tag.tagName.text;
|
|
662
|
+
const memberDisplayNames = /* @__PURE__ */ new Map();
|
|
663
|
+
for (const tag of ts2.getJSDocTags(node)) {
|
|
664
|
+
const tagName = normalizeConstraintTagName(tag.tagName.text);
|
|
665
|
+
if (tagName !== "displayName") continue;
|
|
587
666
|
const commentText = getTagCommentText(tag);
|
|
588
|
-
if (commentText === void 0
|
|
667
|
+
if (commentText === void 0) continue;
|
|
668
|
+
const text = commentText.trim();
|
|
669
|
+
if (text === "") continue;
|
|
670
|
+
const memberTarget = parseMemberTargetDisplayName(text);
|
|
671
|
+
if (memberTarget) {
|
|
672
|
+
memberDisplayNames.set(memberTarget.target, memberTarget.label);
|
|
589
673
|
continue;
|
|
590
674
|
}
|
|
591
|
-
|
|
592
|
-
if (tagName === "Field_displayName") {
|
|
593
|
-
displayName = trimmed;
|
|
594
|
-
displayNameTag = tag;
|
|
595
|
-
} else if (tagName === "Field_description") {
|
|
596
|
-
description = trimmed;
|
|
597
|
-
descriptionTag = tag;
|
|
598
|
-
}
|
|
599
|
-
}
|
|
600
|
-
if (displayName !== void 0 && displayNameTag) {
|
|
601
|
-
annotations.push({
|
|
602
|
-
kind: "annotation",
|
|
603
|
-
annotationKind: "displayName",
|
|
604
|
-
value: displayName,
|
|
605
|
-
provenance: provenanceForJSDocTag(displayNameTag, file)
|
|
606
|
-
});
|
|
607
|
-
}
|
|
608
|
-
if (description !== void 0 && descriptionTag) {
|
|
609
|
-
annotations.push({
|
|
610
|
-
kind: "annotation",
|
|
611
|
-
annotationKind: "description",
|
|
612
|
-
value: description,
|
|
613
|
-
provenance: provenanceForJSDocTag(descriptionTag, file)
|
|
614
|
-
});
|
|
675
|
+
displayName ??= text;
|
|
615
676
|
}
|
|
616
|
-
return {
|
|
677
|
+
return {
|
|
678
|
+
...displayName !== void 0 && { displayName },
|
|
679
|
+
memberDisplayNames
|
|
680
|
+
};
|
|
617
681
|
}
|
|
618
682
|
function extractPathTarget(text) {
|
|
619
683
|
const trimmed = text.trimStart();
|
|
@@ -677,7 +741,45 @@ function parseConstraintValue(tagName, text, provenance) {
|
|
|
677
741
|
}
|
|
678
742
|
return null;
|
|
679
743
|
}
|
|
744
|
+
if (expectedType === "boolean") {
|
|
745
|
+
const trimmed = effectiveText.trim();
|
|
746
|
+
if (trimmed !== "" && trimmed !== "true") {
|
|
747
|
+
return null;
|
|
748
|
+
}
|
|
749
|
+
if (tagName === "uniqueItems") {
|
|
750
|
+
return {
|
|
751
|
+
kind: "constraint",
|
|
752
|
+
constraintKind: "uniqueItems",
|
|
753
|
+
value: true,
|
|
754
|
+
...path2 && { path: path2 },
|
|
755
|
+
provenance
|
|
756
|
+
};
|
|
757
|
+
}
|
|
758
|
+
return null;
|
|
759
|
+
}
|
|
680
760
|
if (expectedType === "json") {
|
|
761
|
+
if (tagName === "const") {
|
|
762
|
+
const trimmedText = effectiveText.trim();
|
|
763
|
+
if (trimmedText === "") return null;
|
|
764
|
+
try {
|
|
765
|
+
const parsed2 = JSON.parse(trimmedText);
|
|
766
|
+
return {
|
|
767
|
+
kind: "constraint",
|
|
768
|
+
constraintKind: "const",
|
|
769
|
+
value: parsed2,
|
|
770
|
+
...path2 && { path: path2 },
|
|
771
|
+
provenance
|
|
772
|
+
};
|
|
773
|
+
} catch {
|
|
774
|
+
return {
|
|
775
|
+
kind: "constraint",
|
|
776
|
+
constraintKind: "const",
|
|
777
|
+
value: trimmedText,
|
|
778
|
+
...path2 && { path: path2 },
|
|
779
|
+
provenance
|
|
780
|
+
};
|
|
781
|
+
}
|
|
782
|
+
}
|
|
681
783
|
const parsed = tryParseJson(effectiveText);
|
|
682
784
|
if (!Array.isArray(parsed)) {
|
|
683
785
|
return null;
|
|
@@ -709,6 +811,34 @@ function parseConstraintValue(tagName, text, provenance) {
|
|
|
709
811
|
provenance
|
|
710
812
|
};
|
|
711
813
|
}
|
|
814
|
+
function parseDefaultValueValue(text, provenance) {
|
|
815
|
+
const trimmed = text.trim();
|
|
816
|
+
let value;
|
|
817
|
+
if (trimmed === "null") {
|
|
818
|
+
value = null;
|
|
819
|
+
} else if (trimmed === "true") {
|
|
820
|
+
value = true;
|
|
821
|
+
} else if (trimmed === "false") {
|
|
822
|
+
value = false;
|
|
823
|
+
} else {
|
|
824
|
+
const parsed = tryParseJson(trimmed);
|
|
825
|
+
value = parsed !== null ? parsed : trimmed;
|
|
826
|
+
}
|
|
827
|
+
return {
|
|
828
|
+
kind: "annotation",
|
|
829
|
+
annotationKind: "defaultValue",
|
|
830
|
+
value,
|
|
831
|
+
provenance
|
|
832
|
+
};
|
|
833
|
+
}
|
|
834
|
+
function isMemberTargetDisplayName(text) {
|
|
835
|
+
return parseMemberTargetDisplayName(text) !== null;
|
|
836
|
+
}
|
|
837
|
+
function parseMemberTargetDisplayName(text) {
|
|
838
|
+
const match = /^:([^\s]+)\s+([\s\S]+)$/.exec(text);
|
|
839
|
+
if (!match?.[1] || !match[2]) return null;
|
|
840
|
+
return { target: match[1], label: match[2].trim() };
|
|
841
|
+
}
|
|
712
842
|
function provenanceForComment(range, sourceFile, file, tagName) {
|
|
713
843
|
const { line, character } = sourceFile.getLineAndCharacterOfPosition(range.pos);
|
|
714
844
|
return {
|
|
@@ -790,11 +920,17 @@ function isObjectType(type) {
|
|
|
790
920
|
function isTypeReference(type) {
|
|
791
921
|
return !!(type.flags & ts4.TypeFlags.Object) && !!(type.objectFlags & ts4.ObjectFlags.Reference);
|
|
792
922
|
}
|
|
923
|
+
var RESOLVING_TYPE_PLACEHOLDER = {
|
|
924
|
+
kind: "object",
|
|
925
|
+
properties: [],
|
|
926
|
+
additionalProperties: true
|
|
927
|
+
};
|
|
793
928
|
function analyzeClassToIR(classDecl, checker, file = "") {
|
|
794
929
|
const name = classDecl.name?.text ?? "AnonymousClass";
|
|
795
930
|
const fields = [];
|
|
796
931
|
const fieldLayouts = [];
|
|
797
932
|
const typeRegistry = {};
|
|
933
|
+
const annotations = extractJSDocAnnotationNodes(classDecl, file);
|
|
798
934
|
const visiting = /* @__PURE__ */ new Set();
|
|
799
935
|
const instanceMethods = [];
|
|
800
936
|
const staticMethods = [];
|
|
@@ -817,12 +953,21 @@ function analyzeClassToIR(classDecl, checker, file = "") {
|
|
|
817
953
|
}
|
|
818
954
|
}
|
|
819
955
|
}
|
|
820
|
-
return {
|
|
956
|
+
return {
|
|
957
|
+
name,
|
|
958
|
+
fields,
|
|
959
|
+
fieldLayouts,
|
|
960
|
+
typeRegistry,
|
|
961
|
+
...annotations.length > 0 && { annotations },
|
|
962
|
+
instanceMethods,
|
|
963
|
+
staticMethods
|
|
964
|
+
};
|
|
821
965
|
}
|
|
822
966
|
function analyzeInterfaceToIR(interfaceDecl, checker, file = "") {
|
|
823
967
|
const name = interfaceDecl.name.text;
|
|
824
968
|
const fields = [];
|
|
825
969
|
const typeRegistry = {};
|
|
970
|
+
const annotations = extractJSDocAnnotationNodes(interfaceDecl, file);
|
|
826
971
|
const visiting = /* @__PURE__ */ new Set();
|
|
827
972
|
for (const member of interfaceDecl.members) {
|
|
828
973
|
if (ts4.isPropertySignature(member)) {
|
|
@@ -833,7 +978,15 @@ function analyzeInterfaceToIR(interfaceDecl, checker, file = "") {
|
|
|
833
978
|
}
|
|
834
979
|
}
|
|
835
980
|
const fieldLayouts = fields.map(() => ({}));
|
|
836
|
-
return {
|
|
981
|
+
return {
|
|
982
|
+
name,
|
|
983
|
+
fields,
|
|
984
|
+
fieldLayouts,
|
|
985
|
+
typeRegistry,
|
|
986
|
+
...annotations.length > 0 && { annotations },
|
|
987
|
+
instanceMethods: [],
|
|
988
|
+
staticMethods: []
|
|
989
|
+
};
|
|
837
990
|
}
|
|
838
991
|
function analyzeTypeAliasToIR(typeAlias, checker, file = "") {
|
|
839
992
|
if (!ts4.isTypeLiteralNode(typeAlias.type)) {
|
|
@@ -848,6 +1001,7 @@ function analyzeTypeAliasToIR(typeAlias, checker, file = "") {
|
|
|
848
1001
|
const name = typeAlias.name.text;
|
|
849
1002
|
const fields = [];
|
|
850
1003
|
const typeRegistry = {};
|
|
1004
|
+
const annotations = extractJSDocAnnotationNodes(typeAlias, file);
|
|
851
1005
|
const visiting = /* @__PURE__ */ new Set();
|
|
852
1006
|
for (const member of typeAlias.type.members) {
|
|
853
1007
|
if (ts4.isPropertySignature(member)) {
|
|
@@ -864,6 +1018,7 @@ function analyzeTypeAliasToIR(typeAlias, checker, file = "") {
|
|
|
864
1018
|
fields,
|
|
865
1019
|
fieldLayouts: fields.map(() => ({})),
|
|
866
1020
|
typeRegistry,
|
|
1021
|
+
...annotations.length > 0 && { annotations },
|
|
867
1022
|
instanceMethods: [],
|
|
868
1023
|
staticMethods: []
|
|
869
1024
|
}
|
|
@@ -877,18 +1032,19 @@ function analyzeFieldToIR(prop, checker, file, typeRegistry, visiting) {
|
|
|
877
1032
|
const tsType = checker.getTypeAtLocation(prop);
|
|
878
1033
|
const optional = prop.questionToken !== void 0;
|
|
879
1034
|
const provenance = provenanceForNode(prop, file);
|
|
880
|
-
|
|
1035
|
+
let type = resolveTypeNode(tsType, checker, file, typeRegistry, visiting, prop);
|
|
881
1036
|
const constraints = [];
|
|
882
1037
|
if (prop.type) {
|
|
883
1038
|
constraints.push(...extractTypeAliasConstraintNodes(prop.type, checker, file));
|
|
884
1039
|
}
|
|
885
1040
|
constraints.push(...extractJSDocConstraintNodes(prop, file));
|
|
886
|
-
|
|
1041
|
+
let annotations = [];
|
|
887
1042
|
annotations.push(...extractJSDocAnnotationNodes(prop, file));
|
|
888
1043
|
const defaultAnnotation = extractDefaultValueAnnotation(prop.initializer, file);
|
|
889
|
-
if (defaultAnnotation) {
|
|
1044
|
+
if (defaultAnnotation && !annotations.some((a) => a.annotationKind === "defaultValue")) {
|
|
890
1045
|
annotations.push(defaultAnnotation);
|
|
891
1046
|
}
|
|
1047
|
+
({ type, annotations } = applyEnumMemberDisplayNames(type, annotations));
|
|
892
1048
|
return {
|
|
893
1049
|
kind: "field",
|
|
894
1050
|
name,
|
|
@@ -907,14 +1063,15 @@ function analyzeInterfacePropertyToIR(prop, checker, file, typeRegistry, visitin
|
|
|
907
1063
|
const tsType = checker.getTypeAtLocation(prop);
|
|
908
1064
|
const optional = prop.questionToken !== void 0;
|
|
909
1065
|
const provenance = provenanceForNode(prop, file);
|
|
910
|
-
|
|
1066
|
+
let type = resolveTypeNode(tsType, checker, file, typeRegistry, visiting, prop);
|
|
911
1067
|
const constraints = [];
|
|
912
1068
|
if (prop.type) {
|
|
913
1069
|
constraints.push(...extractTypeAliasConstraintNodes(prop.type, checker, file));
|
|
914
1070
|
}
|
|
915
1071
|
constraints.push(...extractJSDocConstraintNodes(prop, file));
|
|
916
|
-
|
|
1072
|
+
let annotations = [];
|
|
917
1073
|
annotations.push(...extractJSDocAnnotationNodes(prop, file));
|
|
1074
|
+
({ type, annotations } = applyEnumMemberDisplayNames(type, annotations));
|
|
918
1075
|
return {
|
|
919
1076
|
kind: "field",
|
|
920
1077
|
name,
|
|
@@ -925,7 +1082,69 @@ function analyzeInterfacePropertyToIR(prop, checker, file, typeRegistry, visitin
|
|
|
925
1082
|
provenance
|
|
926
1083
|
};
|
|
927
1084
|
}
|
|
928
|
-
function
|
|
1085
|
+
function applyEnumMemberDisplayNames(type, annotations) {
|
|
1086
|
+
if (!annotations.some(
|
|
1087
|
+
(annotation) => annotation.annotationKind === "displayName" && annotation.value.trim().startsWith(":")
|
|
1088
|
+
)) {
|
|
1089
|
+
return { type, annotations: [...annotations] };
|
|
1090
|
+
}
|
|
1091
|
+
const consumed = /* @__PURE__ */ new Set();
|
|
1092
|
+
const nextType = rewriteEnumDisplayNames(type, annotations, consumed);
|
|
1093
|
+
if (consumed.size === 0) {
|
|
1094
|
+
return { type, annotations: [...annotations] };
|
|
1095
|
+
}
|
|
1096
|
+
return {
|
|
1097
|
+
type: nextType,
|
|
1098
|
+
annotations: annotations.filter((annotation) => !consumed.has(annotation))
|
|
1099
|
+
};
|
|
1100
|
+
}
|
|
1101
|
+
function rewriteEnumDisplayNames(type, annotations, consumed) {
|
|
1102
|
+
switch (type.kind) {
|
|
1103
|
+
case "enum":
|
|
1104
|
+
return applyEnumMemberDisplayNamesToEnum(type, annotations, consumed);
|
|
1105
|
+
case "union": {
|
|
1106
|
+
return {
|
|
1107
|
+
...type,
|
|
1108
|
+
members: type.members.map(
|
|
1109
|
+
(member) => rewriteEnumDisplayNames(member, annotations, consumed)
|
|
1110
|
+
)
|
|
1111
|
+
};
|
|
1112
|
+
}
|
|
1113
|
+
default:
|
|
1114
|
+
return type;
|
|
1115
|
+
}
|
|
1116
|
+
}
|
|
1117
|
+
function applyEnumMemberDisplayNamesToEnum(type, annotations, consumed) {
|
|
1118
|
+
const displayNames = /* @__PURE__ */ new Map();
|
|
1119
|
+
for (const annotation of annotations) {
|
|
1120
|
+
if (annotation.annotationKind !== "displayName") continue;
|
|
1121
|
+
const parsed = parseEnumMemberDisplayName(annotation.value);
|
|
1122
|
+
if (!parsed) continue;
|
|
1123
|
+
consumed.add(annotation);
|
|
1124
|
+
const member = type.members.find((m) => String(m.value) === parsed.value);
|
|
1125
|
+
if (!member) continue;
|
|
1126
|
+
displayNames.set(String(member.value), parsed.label);
|
|
1127
|
+
}
|
|
1128
|
+
if (displayNames.size === 0) {
|
|
1129
|
+
return type;
|
|
1130
|
+
}
|
|
1131
|
+
return {
|
|
1132
|
+
...type,
|
|
1133
|
+
members: type.members.map((member) => {
|
|
1134
|
+
const displayName = displayNames.get(String(member.value));
|
|
1135
|
+
return displayName !== void 0 ? { ...member, displayName } : member;
|
|
1136
|
+
})
|
|
1137
|
+
};
|
|
1138
|
+
}
|
|
1139
|
+
function parseEnumMemberDisplayName(value) {
|
|
1140
|
+
const trimmed = value.trim();
|
|
1141
|
+
const match = /^:([^\s]+)\s+([\s\S]+)$/.exec(trimmed);
|
|
1142
|
+
if (!match?.[1] || !match[2]) return null;
|
|
1143
|
+
const label = match[2].trim();
|
|
1144
|
+
if (label === "") return null;
|
|
1145
|
+
return { value: match[1], label };
|
|
1146
|
+
}
|
|
1147
|
+
function resolveTypeNode(type, checker, file, typeRegistry, visiting, sourceNode) {
|
|
929
1148
|
if (type.flags & ts4.TypeFlags.String) {
|
|
930
1149
|
return { kind: "primitive", primitiveKind: "string" };
|
|
931
1150
|
}
|
|
@@ -954,7 +1173,7 @@ function resolveTypeNode(type, checker, file, typeRegistry, visiting) {
|
|
|
954
1173
|
};
|
|
955
1174
|
}
|
|
956
1175
|
if (type.isUnion()) {
|
|
957
|
-
return resolveUnionType(type, checker, file, typeRegistry, visiting);
|
|
1176
|
+
return resolveUnionType(type, checker, file, typeRegistry, visiting, sourceNode);
|
|
958
1177
|
}
|
|
959
1178
|
if (checker.isArrayType(type)) {
|
|
960
1179
|
return resolveArrayType(type, checker, file, typeRegistry, visiting);
|
|
@@ -964,70 +1183,102 @@ function resolveTypeNode(type, checker, file, typeRegistry, visiting) {
|
|
|
964
1183
|
}
|
|
965
1184
|
return { kind: "primitive", primitiveKind: "string" };
|
|
966
1185
|
}
|
|
967
|
-
function resolveUnionType(type, checker, file, typeRegistry, visiting) {
|
|
1186
|
+
function resolveUnionType(type, checker, file, typeRegistry, visiting, sourceNode) {
|
|
1187
|
+
const typeName = getNamedTypeName(type);
|
|
1188
|
+
const namedDecl = getNamedTypeDeclaration(type);
|
|
1189
|
+
if (typeName && typeName in typeRegistry) {
|
|
1190
|
+
return { kind: "reference", name: typeName, typeArguments: [] };
|
|
1191
|
+
}
|
|
968
1192
|
const allTypes = type.types;
|
|
969
1193
|
const nonNullTypes = allTypes.filter(
|
|
970
1194
|
(t) => !(t.flags & (ts4.TypeFlags.Null | ts4.TypeFlags.Undefined))
|
|
971
1195
|
);
|
|
972
1196
|
const hasNull = allTypes.some((t) => t.flags & ts4.TypeFlags.Null);
|
|
1197
|
+
const memberDisplayNames = /* @__PURE__ */ new Map();
|
|
1198
|
+
if (namedDecl) {
|
|
1199
|
+
for (const [value, label] of extractDisplayNameMetadata(namedDecl).memberDisplayNames) {
|
|
1200
|
+
memberDisplayNames.set(value, label);
|
|
1201
|
+
}
|
|
1202
|
+
}
|
|
1203
|
+
if (sourceNode) {
|
|
1204
|
+
for (const [value, label] of extractDisplayNameMetadata(sourceNode).memberDisplayNames) {
|
|
1205
|
+
memberDisplayNames.set(value, label);
|
|
1206
|
+
}
|
|
1207
|
+
}
|
|
1208
|
+
const registerNamed = (result) => {
|
|
1209
|
+
if (!typeName) {
|
|
1210
|
+
return result;
|
|
1211
|
+
}
|
|
1212
|
+
const annotations = namedDecl ? extractJSDocAnnotationNodes(namedDecl, file) : void 0;
|
|
1213
|
+
typeRegistry[typeName] = {
|
|
1214
|
+
name: typeName,
|
|
1215
|
+
type: result,
|
|
1216
|
+
...annotations !== void 0 && annotations.length > 0 && { annotations },
|
|
1217
|
+
provenance: provenanceForDeclaration(namedDecl ?? sourceNode, file)
|
|
1218
|
+
};
|
|
1219
|
+
return { kind: "reference", name: typeName, typeArguments: [] };
|
|
1220
|
+
};
|
|
1221
|
+
const applyMemberLabels = (members2) => members2.map((value) => {
|
|
1222
|
+
const displayName = memberDisplayNames.get(String(value));
|
|
1223
|
+
return displayName !== void 0 ? { value, displayName } : { value };
|
|
1224
|
+
});
|
|
973
1225
|
const isBooleanUnion2 = nonNullTypes.length === 2 && nonNullTypes.every((t) => t.flags & ts4.TypeFlags.BooleanLiteral);
|
|
974
1226
|
if (isBooleanUnion2) {
|
|
975
1227
|
const boolNode = { kind: "primitive", primitiveKind: "boolean" };
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
}
|
|
982
|
-
return boolNode;
|
|
1228
|
+
const result = hasNull ? {
|
|
1229
|
+
kind: "union",
|
|
1230
|
+
members: [boolNode, { kind: "primitive", primitiveKind: "null" }]
|
|
1231
|
+
} : boolNode;
|
|
1232
|
+
return registerNamed(result);
|
|
983
1233
|
}
|
|
984
1234
|
const allStringLiterals = nonNullTypes.every((t) => t.isStringLiteral());
|
|
985
1235
|
if (allStringLiterals && nonNullTypes.length > 0) {
|
|
986
1236
|
const stringTypes = nonNullTypes.filter((t) => t.isStringLiteral());
|
|
987
1237
|
const enumNode = {
|
|
988
1238
|
kind: "enum",
|
|
989
|
-
members: stringTypes.map((t) =>
|
|
1239
|
+
members: applyMemberLabels(stringTypes.map((t) => t.value))
|
|
990
1240
|
};
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
}
|
|
997
|
-
return enumNode;
|
|
1241
|
+
const result = hasNull ? {
|
|
1242
|
+
kind: "union",
|
|
1243
|
+
members: [enumNode, { kind: "primitive", primitiveKind: "null" }]
|
|
1244
|
+
} : enumNode;
|
|
1245
|
+
return registerNamed(result);
|
|
998
1246
|
}
|
|
999
1247
|
const allNumberLiterals = nonNullTypes.every((t) => t.isNumberLiteral());
|
|
1000
1248
|
if (allNumberLiterals && nonNullTypes.length > 0) {
|
|
1001
1249
|
const numberTypes = nonNullTypes.filter((t) => t.isNumberLiteral());
|
|
1002
1250
|
const enumNode = {
|
|
1003
1251
|
kind: "enum",
|
|
1004
|
-
members: numberTypes.map((t) =>
|
|
1252
|
+
members: applyMemberLabels(numberTypes.map((t) => t.value))
|
|
1005
1253
|
};
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
}
|
|
1012
|
-
return enumNode;
|
|
1254
|
+
const result = hasNull ? {
|
|
1255
|
+
kind: "union",
|
|
1256
|
+
members: [enumNode, { kind: "primitive", primitiveKind: "null" }]
|
|
1257
|
+
} : enumNode;
|
|
1258
|
+
return registerNamed(result);
|
|
1013
1259
|
}
|
|
1014
1260
|
if (nonNullTypes.length === 1 && nonNullTypes[0]) {
|
|
1015
|
-
const inner = resolveTypeNode(
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1261
|
+
const inner = resolveTypeNode(
|
|
1262
|
+
nonNullTypes[0],
|
|
1263
|
+
checker,
|
|
1264
|
+
file,
|
|
1265
|
+
typeRegistry,
|
|
1266
|
+
visiting,
|
|
1267
|
+
sourceNode
|
|
1268
|
+
);
|
|
1269
|
+
const result = hasNull ? {
|
|
1270
|
+
kind: "union",
|
|
1271
|
+
members: [inner, { kind: "primitive", primitiveKind: "null" }]
|
|
1272
|
+
} : inner;
|
|
1273
|
+
return registerNamed(result);
|
|
1023
1274
|
}
|
|
1024
1275
|
const members = nonNullTypes.map(
|
|
1025
|
-
(t) => resolveTypeNode(t, checker, file, typeRegistry, visiting)
|
|
1276
|
+
(t) => resolveTypeNode(t, checker, file, typeRegistry, visiting, sourceNode)
|
|
1026
1277
|
);
|
|
1027
1278
|
if (hasNull) {
|
|
1028
1279
|
members.push({ kind: "primitive", primitiveKind: "null" });
|
|
1029
1280
|
}
|
|
1030
|
-
return { kind: "union", members };
|
|
1281
|
+
return registerNamed({ kind: "union", members });
|
|
1031
1282
|
}
|
|
1032
1283
|
function resolveArrayType(type, checker, file, typeRegistry, visiting) {
|
|
1033
1284
|
const typeArgs = isTypeReference(type) ? type.typeArguments : void 0;
|
|
@@ -1035,15 +1286,92 @@ function resolveArrayType(type, checker, file, typeRegistry, visiting) {
|
|
|
1035
1286
|
const items = elementType ? resolveTypeNode(elementType, checker, file, typeRegistry, visiting) : { kind: "primitive", primitiveKind: "string" };
|
|
1036
1287
|
return { kind: "array", items };
|
|
1037
1288
|
}
|
|
1289
|
+
function tryResolveRecordType(type, checker, file, typeRegistry, visiting) {
|
|
1290
|
+
if (type.getProperties().length > 0) {
|
|
1291
|
+
return null;
|
|
1292
|
+
}
|
|
1293
|
+
const indexInfo = checker.getIndexInfoOfType(type, ts4.IndexKind.String);
|
|
1294
|
+
if (!indexInfo) {
|
|
1295
|
+
return null;
|
|
1296
|
+
}
|
|
1297
|
+
const valueType = resolveTypeNode(indexInfo.type, checker, file, typeRegistry, visiting);
|
|
1298
|
+
return { kind: "record", valueType };
|
|
1299
|
+
}
|
|
1300
|
+
function typeNodeContainsReference(type, targetName) {
|
|
1301
|
+
switch (type.kind) {
|
|
1302
|
+
case "reference":
|
|
1303
|
+
return type.name === targetName;
|
|
1304
|
+
case "array":
|
|
1305
|
+
return typeNodeContainsReference(type.items, targetName);
|
|
1306
|
+
case "record":
|
|
1307
|
+
return typeNodeContainsReference(type.valueType, targetName);
|
|
1308
|
+
case "union":
|
|
1309
|
+
return type.members.some((member) => typeNodeContainsReference(member, targetName));
|
|
1310
|
+
case "object":
|
|
1311
|
+
return type.properties.some(
|
|
1312
|
+
(property) => typeNodeContainsReference(property.type, targetName)
|
|
1313
|
+
);
|
|
1314
|
+
case "primitive":
|
|
1315
|
+
case "enum":
|
|
1316
|
+
case "dynamic":
|
|
1317
|
+
case "custom":
|
|
1318
|
+
return false;
|
|
1319
|
+
default: {
|
|
1320
|
+
const _exhaustive = type;
|
|
1321
|
+
return _exhaustive;
|
|
1322
|
+
}
|
|
1323
|
+
}
|
|
1324
|
+
}
|
|
1038
1325
|
function resolveObjectType(type, checker, file, typeRegistry, visiting) {
|
|
1326
|
+
const typeName = getNamedTypeName(type);
|
|
1327
|
+
const namedTypeName = typeName ?? void 0;
|
|
1328
|
+
const namedDecl = getNamedTypeDeclaration(type);
|
|
1329
|
+
const shouldRegisterNamedType = namedTypeName !== void 0 && !(namedTypeName === "Record" && namedDecl?.getSourceFile().fileName !== file);
|
|
1330
|
+
const clearNamedTypeRegistration = () => {
|
|
1331
|
+
if (namedTypeName === void 0 || !shouldRegisterNamedType) {
|
|
1332
|
+
return;
|
|
1333
|
+
}
|
|
1334
|
+
Reflect.deleteProperty(typeRegistry, namedTypeName);
|
|
1335
|
+
};
|
|
1039
1336
|
if (visiting.has(type)) {
|
|
1337
|
+
if (namedTypeName !== void 0 && shouldRegisterNamedType) {
|
|
1338
|
+
return { kind: "reference", name: namedTypeName, typeArguments: [] };
|
|
1339
|
+
}
|
|
1040
1340
|
return { kind: "object", properties: [], additionalProperties: false };
|
|
1041
1341
|
}
|
|
1342
|
+
if (namedTypeName !== void 0 && shouldRegisterNamedType && !typeRegistry[namedTypeName]) {
|
|
1343
|
+
typeRegistry[namedTypeName] = {
|
|
1344
|
+
name: namedTypeName,
|
|
1345
|
+
type: RESOLVING_TYPE_PLACEHOLDER,
|
|
1346
|
+
provenance: provenanceForDeclaration(namedDecl, file)
|
|
1347
|
+
};
|
|
1348
|
+
}
|
|
1042
1349
|
visiting.add(type);
|
|
1043
|
-
|
|
1044
|
-
|
|
1350
|
+
if (namedTypeName !== void 0 && shouldRegisterNamedType && typeRegistry[namedTypeName]?.type !== void 0) {
|
|
1351
|
+
if (typeRegistry[namedTypeName].type !== RESOLVING_TYPE_PLACEHOLDER) {
|
|
1352
|
+
visiting.delete(type);
|
|
1353
|
+
return { kind: "reference", name: namedTypeName, typeArguments: [] };
|
|
1354
|
+
}
|
|
1355
|
+
}
|
|
1356
|
+
const recordNode = tryResolveRecordType(type, checker, file, typeRegistry, visiting);
|
|
1357
|
+
if (recordNode) {
|
|
1045
1358
|
visiting.delete(type);
|
|
1046
|
-
|
|
1359
|
+
if (namedTypeName !== void 0 && shouldRegisterNamedType) {
|
|
1360
|
+
const isRecursiveRecord = typeNodeContainsReference(recordNode.valueType, namedTypeName);
|
|
1361
|
+
if (!isRecursiveRecord) {
|
|
1362
|
+
clearNamedTypeRegistration();
|
|
1363
|
+
return recordNode;
|
|
1364
|
+
}
|
|
1365
|
+
const annotations = namedDecl ? extractJSDocAnnotationNodes(namedDecl, file) : void 0;
|
|
1366
|
+
typeRegistry[namedTypeName] = {
|
|
1367
|
+
name: namedTypeName,
|
|
1368
|
+
type: recordNode,
|
|
1369
|
+
...annotations !== void 0 && annotations.length > 0 && { annotations },
|
|
1370
|
+
provenance: provenanceForDeclaration(namedDecl, file)
|
|
1371
|
+
};
|
|
1372
|
+
return { kind: "reference", name: namedTypeName, typeArguments: [] };
|
|
1373
|
+
}
|
|
1374
|
+
return recordNode;
|
|
1047
1375
|
}
|
|
1048
1376
|
const properties = [];
|
|
1049
1377
|
const fieldInfoMap = getNamedTypeFieldNodeInfoMap(type, checker, file, typeRegistry, visiting);
|
|
@@ -1052,7 +1380,14 @@ function resolveObjectType(type, checker, file, typeRegistry, visiting) {
|
|
|
1052
1380
|
if (!declaration) continue;
|
|
1053
1381
|
const propType = checker.getTypeOfSymbolAtLocation(prop, declaration);
|
|
1054
1382
|
const optional = !!(prop.flags & ts4.SymbolFlags.Optional);
|
|
1055
|
-
const propTypeNode = resolveTypeNode(
|
|
1383
|
+
const propTypeNode = resolveTypeNode(
|
|
1384
|
+
propType,
|
|
1385
|
+
checker,
|
|
1386
|
+
file,
|
|
1387
|
+
typeRegistry,
|
|
1388
|
+
visiting,
|
|
1389
|
+
declaration
|
|
1390
|
+
);
|
|
1056
1391
|
const fieldNodeInfo = fieldInfoMap?.get(prop.name);
|
|
1057
1392
|
properties.push({
|
|
1058
1393
|
name: prop.name,
|
|
@@ -1067,15 +1402,17 @@ function resolveObjectType(type, checker, file, typeRegistry, visiting) {
|
|
|
1067
1402
|
const objectNode = {
|
|
1068
1403
|
kind: "object",
|
|
1069
1404
|
properties,
|
|
1070
|
-
additionalProperties:
|
|
1405
|
+
additionalProperties: true
|
|
1071
1406
|
};
|
|
1072
|
-
if (
|
|
1073
|
-
|
|
1074
|
-
|
|
1407
|
+
if (namedTypeName !== void 0 && shouldRegisterNamedType) {
|
|
1408
|
+
const annotations = namedDecl ? extractJSDocAnnotationNodes(namedDecl, file) : void 0;
|
|
1409
|
+
typeRegistry[namedTypeName] = {
|
|
1410
|
+
name: namedTypeName,
|
|
1075
1411
|
type: objectNode,
|
|
1076
|
-
|
|
1412
|
+
...annotations !== void 0 && annotations.length > 0 && { annotations },
|
|
1413
|
+
provenance: provenanceForDeclaration(namedDecl, file)
|
|
1077
1414
|
};
|
|
1078
|
-
return { kind: "reference", name:
|
|
1415
|
+
return { kind: "reference", name: namedTypeName, typeArguments: [] };
|
|
1079
1416
|
}
|
|
1080
1417
|
return objectNode;
|
|
1081
1418
|
}
|
|
@@ -1167,6 +1504,12 @@ function provenanceForNode(node, file) {
|
|
|
1167
1504
|
function provenanceForFile(file) {
|
|
1168
1505
|
return { surface: "tsdoc", file, line: 0, column: 0 };
|
|
1169
1506
|
}
|
|
1507
|
+
function provenanceForDeclaration(node, file) {
|
|
1508
|
+
if (!node) {
|
|
1509
|
+
return provenanceForFile(file);
|
|
1510
|
+
}
|
|
1511
|
+
return provenanceForNode(node, file);
|
|
1512
|
+
}
|
|
1170
1513
|
function getNamedTypeName(type) {
|
|
1171
1514
|
const symbol = type.getSymbol();
|
|
1172
1515
|
if (symbol?.declarations) {
|
|
@@ -1185,6 +1528,20 @@ function getNamedTypeName(type) {
|
|
|
1185
1528
|
}
|
|
1186
1529
|
return null;
|
|
1187
1530
|
}
|
|
1531
|
+
function getNamedTypeDeclaration(type) {
|
|
1532
|
+
const symbol = type.getSymbol();
|
|
1533
|
+
if (symbol?.declarations) {
|
|
1534
|
+
const decl = symbol.declarations[0];
|
|
1535
|
+
if (decl && (ts4.isClassDeclaration(decl) || ts4.isInterfaceDeclaration(decl) || ts4.isTypeAliasDeclaration(decl))) {
|
|
1536
|
+
return decl;
|
|
1537
|
+
}
|
|
1538
|
+
}
|
|
1539
|
+
const aliasSymbol = type.aliasSymbol;
|
|
1540
|
+
if (aliasSymbol?.declarations) {
|
|
1541
|
+
return aliasSymbol.declarations.find(ts4.isTypeAliasDeclaration);
|
|
1542
|
+
}
|
|
1543
|
+
return void 0;
|
|
1544
|
+
}
|
|
1188
1545
|
function analyzeMethod(method, checker) {
|
|
1189
1546
|
if (!ts4.isIdentifier(method.name)) {
|
|
1190
1547
|
return null;
|
|
@@ -1227,13 +1584,26 @@ function detectFormSpecReference(typeNode) {
|
|
|
1227
1584
|
}
|
|
1228
1585
|
|
|
1229
1586
|
// src/json-schema/ir-generator.ts
|
|
1230
|
-
function makeContext() {
|
|
1231
|
-
|
|
1587
|
+
function makeContext(options) {
|
|
1588
|
+
const vendorPrefix = options?.vendorPrefix ?? "x-formspec";
|
|
1589
|
+
if (!vendorPrefix.startsWith("x-")) {
|
|
1590
|
+
throw new Error(
|
|
1591
|
+
`Invalid vendorPrefix "${vendorPrefix}". Extension JSON Schema keywords must start with "x-".`
|
|
1592
|
+
);
|
|
1593
|
+
}
|
|
1594
|
+
return {
|
|
1595
|
+
defs: {},
|
|
1596
|
+
extensionRegistry: options?.extensionRegistry,
|
|
1597
|
+
vendorPrefix
|
|
1598
|
+
};
|
|
1232
1599
|
}
|
|
1233
|
-
function generateJsonSchemaFromIR(ir) {
|
|
1234
|
-
const ctx = makeContext();
|
|
1600
|
+
function generateJsonSchemaFromIR(ir, options) {
|
|
1601
|
+
const ctx = makeContext(options);
|
|
1235
1602
|
for (const [name, typeDef] of Object.entries(ir.typeRegistry)) {
|
|
1236
1603
|
ctx.defs[name] = generateTypeNode(typeDef.type, ctx);
|
|
1604
|
+
if (typeDef.annotations && typeDef.annotations.length > 0) {
|
|
1605
|
+
applyAnnotations(ctx.defs[name], typeDef.annotations, ctx);
|
|
1606
|
+
}
|
|
1237
1607
|
}
|
|
1238
1608
|
const properties = {};
|
|
1239
1609
|
const required = [];
|
|
@@ -1245,6 +1615,9 @@ function generateJsonSchemaFromIR(ir) {
|
|
|
1245
1615
|
properties,
|
|
1246
1616
|
...uniqueRequired.length > 0 && { required: uniqueRequired }
|
|
1247
1617
|
};
|
|
1618
|
+
if (ir.annotations && ir.annotations.length > 0) {
|
|
1619
|
+
applyAnnotations(result, ir.annotations, ctx);
|
|
1620
|
+
}
|
|
1248
1621
|
if (Object.keys(ctx.defs).length > 0) {
|
|
1249
1622
|
result.$defs = ctx.defs;
|
|
1250
1623
|
}
|
|
@@ -1274,25 +1647,54 @@ function collectFields(elements, properties, required, ctx) {
|
|
|
1274
1647
|
}
|
|
1275
1648
|
function generateFieldSchema(field, ctx) {
|
|
1276
1649
|
const schema = generateTypeNode(field.type, ctx);
|
|
1650
|
+
const itemStringSchema = schema.type === "array" && schema.items?.type === "string" ? schema.items : void 0;
|
|
1277
1651
|
const directConstraints = [];
|
|
1652
|
+
const itemConstraints = [];
|
|
1278
1653
|
const pathConstraints = [];
|
|
1279
1654
|
for (const c of field.constraints) {
|
|
1280
1655
|
if (c.path) {
|
|
1281
1656
|
pathConstraints.push(c);
|
|
1657
|
+
} else if (itemStringSchema !== void 0 && isStringItemConstraint(c)) {
|
|
1658
|
+
itemConstraints.push(c);
|
|
1282
1659
|
} else {
|
|
1283
1660
|
directConstraints.push(c);
|
|
1284
1661
|
}
|
|
1285
1662
|
}
|
|
1286
|
-
applyConstraints(schema, directConstraints);
|
|
1287
|
-
|
|
1663
|
+
applyConstraints(schema, directConstraints, ctx);
|
|
1664
|
+
if (itemStringSchema !== void 0) {
|
|
1665
|
+
applyConstraints(itemStringSchema, itemConstraints, ctx);
|
|
1666
|
+
}
|
|
1667
|
+
const rootAnnotations = [];
|
|
1668
|
+
const itemAnnotations = [];
|
|
1669
|
+
for (const annotation of field.annotations) {
|
|
1670
|
+
if (itemStringSchema !== void 0 && annotation.annotationKind === "format") {
|
|
1671
|
+
itemAnnotations.push(annotation);
|
|
1672
|
+
} else {
|
|
1673
|
+
rootAnnotations.push(annotation);
|
|
1674
|
+
}
|
|
1675
|
+
}
|
|
1676
|
+
applyAnnotations(schema, rootAnnotations, ctx);
|
|
1677
|
+
if (itemStringSchema !== void 0) {
|
|
1678
|
+
applyAnnotations(itemStringSchema, itemAnnotations, ctx);
|
|
1679
|
+
}
|
|
1288
1680
|
if (pathConstraints.length === 0) {
|
|
1289
1681
|
return schema;
|
|
1290
1682
|
}
|
|
1291
|
-
return applyPathTargetedConstraints(schema, pathConstraints);
|
|
1683
|
+
return applyPathTargetedConstraints(schema, pathConstraints, ctx);
|
|
1292
1684
|
}
|
|
1293
|
-
function
|
|
1685
|
+
function isStringItemConstraint(constraint) {
|
|
1686
|
+
switch (constraint.constraintKind) {
|
|
1687
|
+
case "minLength":
|
|
1688
|
+
case "maxLength":
|
|
1689
|
+
case "pattern":
|
|
1690
|
+
return true;
|
|
1691
|
+
default:
|
|
1692
|
+
return false;
|
|
1693
|
+
}
|
|
1694
|
+
}
|
|
1695
|
+
function applyPathTargetedConstraints(schema, pathConstraints, ctx) {
|
|
1294
1696
|
if (schema.type === "array" && schema.items) {
|
|
1295
|
-
schema.items = applyPathTargetedConstraints(schema.items, pathConstraints);
|
|
1697
|
+
schema.items = applyPathTargetedConstraints(schema.items, pathConstraints, ctx);
|
|
1296
1698
|
return schema;
|
|
1297
1699
|
}
|
|
1298
1700
|
const byTarget = /* @__PURE__ */ new Map();
|
|
@@ -1306,7 +1708,7 @@ function applyPathTargetedConstraints(schema, pathConstraints) {
|
|
|
1306
1708
|
const propertyOverrides = {};
|
|
1307
1709
|
for (const [target, constraints] of byTarget) {
|
|
1308
1710
|
const subSchema = {};
|
|
1309
|
-
applyConstraints(subSchema, constraints);
|
|
1711
|
+
applyConstraints(subSchema, constraints, ctx);
|
|
1310
1712
|
propertyOverrides[target] = subSchema;
|
|
1311
1713
|
}
|
|
1312
1714
|
if (schema.$ref) {
|
|
@@ -1350,6 +1752,8 @@ function generateTypeNode(type, ctx) {
|
|
|
1350
1752
|
return generateArrayType(type, ctx);
|
|
1351
1753
|
case "object":
|
|
1352
1754
|
return generateObjectType(type, ctx);
|
|
1755
|
+
case "record":
|
|
1756
|
+
return generateRecordType(type, ctx);
|
|
1353
1757
|
case "union":
|
|
1354
1758
|
return generateUnionType(type, ctx);
|
|
1355
1759
|
case "reference":
|
|
@@ -1357,7 +1761,7 @@ function generateTypeNode(type, ctx) {
|
|
|
1357
1761
|
case "dynamic":
|
|
1358
1762
|
return generateDynamicType(type);
|
|
1359
1763
|
case "custom":
|
|
1360
|
-
return generateCustomType(type);
|
|
1764
|
+
return generateCustomType(type, ctx);
|
|
1361
1765
|
default: {
|
|
1362
1766
|
const _exhaustive = type;
|
|
1363
1767
|
return _exhaustive;
|
|
@@ -1406,16 +1810,27 @@ function generateObjectType(type, ctx) {
|
|
|
1406
1810
|
}
|
|
1407
1811
|
return schema;
|
|
1408
1812
|
}
|
|
1813
|
+
function generateRecordType(type, ctx) {
|
|
1814
|
+
return {
|
|
1815
|
+
type: "object",
|
|
1816
|
+
additionalProperties: generateTypeNode(type.valueType, ctx)
|
|
1817
|
+
};
|
|
1818
|
+
}
|
|
1409
1819
|
function generatePropertySchema(prop, ctx) {
|
|
1410
1820
|
const schema = generateTypeNode(prop.type, ctx);
|
|
1411
|
-
applyConstraints(schema, prop.constraints);
|
|
1412
|
-
applyAnnotations(schema, prop.annotations);
|
|
1821
|
+
applyConstraints(schema, prop.constraints, ctx);
|
|
1822
|
+
applyAnnotations(schema, prop.annotations, ctx);
|
|
1413
1823
|
return schema;
|
|
1414
1824
|
}
|
|
1415
1825
|
function generateUnionType(type, ctx) {
|
|
1416
1826
|
if (isBooleanUnion(type)) {
|
|
1417
1827
|
return { type: "boolean" };
|
|
1418
1828
|
}
|
|
1829
|
+
if (isNullableUnion(type)) {
|
|
1830
|
+
return {
|
|
1831
|
+
oneOf: type.members.map((m) => generateTypeNode(m, ctx))
|
|
1832
|
+
};
|
|
1833
|
+
}
|
|
1419
1834
|
return {
|
|
1420
1835
|
anyOf: type.members.map((m) => generateTypeNode(m, ctx))
|
|
1421
1836
|
};
|
|
@@ -1425,6 +1840,13 @@ function isBooleanUnion(type) {
|
|
|
1425
1840
|
const kinds = type.members.map((m) => m.kind);
|
|
1426
1841
|
return kinds.every((k) => k === "primitive") && type.members.every((m) => m.kind === "primitive" && m.primitiveKind === "boolean");
|
|
1427
1842
|
}
|
|
1843
|
+
function isNullableUnion(type) {
|
|
1844
|
+
if (type.members.length !== 2) return false;
|
|
1845
|
+
const nullCount = type.members.filter(
|
|
1846
|
+
(m) => m.kind === "primitive" && m.primitiveKind === "null"
|
|
1847
|
+
).length;
|
|
1848
|
+
return nullCount === 1;
|
|
1849
|
+
}
|
|
1428
1850
|
function generateReferenceType(type) {
|
|
1429
1851
|
return { $ref: `#/$defs/${type.name}` };
|
|
1430
1852
|
}
|
|
@@ -1445,10 +1867,7 @@ function generateDynamicType(type) {
|
|
|
1445
1867
|
"x-formspec-schemaSource": type.sourceKey
|
|
1446
1868
|
};
|
|
1447
1869
|
}
|
|
1448
|
-
function
|
|
1449
|
-
return { type: "object" };
|
|
1450
|
-
}
|
|
1451
|
-
function applyConstraints(schema, constraints) {
|
|
1870
|
+
function applyConstraints(schema, constraints, ctx) {
|
|
1452
1871
|
for (const constraint of constraints) {
|
|
1453
1872
|
switch (constraint.constraintKind) {
|
|
1454
1873
|
case "minimum":
|
|
@@ -1490,9 +1909,13 @@ function applyConstraints(schema, constraints) {
|
|
|
1490
1909
|
case "uniqueItems":
|
|
1491
1910
|
schema.uniqueItems = constraint.value;
|
|
1492
1911
|
break;
|
|
1912
|
+
case "const":
|
|
1913
|
+
schema.const = constraint.value;
|
|
1914
|
+
break;
|
|
1493
1915
|
case "allowedMembers":
|
|
1494
1916
|
break;
|
|
1495
1917
|
case "custom":
|
|
1918
|
+
applyCustomConstraint(schema, constraint, ctx);
|
|
1496
1919
|
break;
|
|
1497
1920
|
default: {
|
|
1498
1921
|
const _exhaustive = constraint;
|
|
@@ -1501,7 +1924,7 @@ function applyConstraints(schema, constraints) {
|
|
|
1501
1924
|
}
|
|
1502
1925
|
}
|
|
1503
1926
|
}
|
|
1504
|
-
function applyAnnotations(schema, annotations) {
|
|
1927
|
+
function applyAnnotations(schema, annotations, ctx) {
|
|
1505
1928
|
for (const annotation of annotations) {
|
|
1506
1929
|
switch (annotation.annotationKind) {
|
|
1507
1930
|
case "displayName":
|
|
@@ -1513,14 +1936,21 @@ function applyAnnotations(schema, annotations) {
|
|
|
1513
1936
|
case "defaultValue":
|
|
1514
1937
|
schema.default = annotation.value;
|
|
1515
1938
|
break;
|
|
1939
|
+
case "format":
|
|
1940
|
+
schema.format = annotation.value;
|
|
1941
|
+
break;
|
|
1516
1942
|
case "deprecated":
|
|
1517
1943
|
schema.deprecated = true;
|
|
1944
|
+
if (annotation.message !== void 0 && annotation.message !== "") {
|
|
1945
|
+
schema["x-formspec-deprecation-description"] = annotation.message;
|
|
1946
|
+
}
|
|
1518
1947
|
break;
|
|
1519
1948
|
case "placeholder":
|
|
1520
1949
|
break;
|
|
1521
1950
|
case "formatHint":
|
|
1522
1951
|
break;
|
|
1523
1952
|
case "custom":
|
|
1953
|
+
applyCustomAnnotation(schema, annotation, ctx);
|
|
1524
1954
|
break;
|
|
1525
1955
|
default: {
|
|
1526
1956
|
const _exhaustive = annotation;
|
|
@@ -1529,6 +1959,36 @@ function applyAnnotations(schema, annotations) {
|
|
|
1529
1959
|
}
|
|
1530
1960
|
}
|
|
1531
1961
|
}
|
|
1962
|
+
function generateCustomType(type, ctx) {
|
|
1963
|
+
const registration = ctx.extensionRegistry?.findType(type.typeId);
|
|
1964
|
+
if (registration === void 0) {
|
|
1965
|
+
throw new Error(
|
|
1966
|
+
`Cannot generate JSON Schema for custom type "${type.typeId}" without a matching extension registration`
|
|
1967
|
+
);
|
|
1968
|
+
}
|
|
1969
|
+
return registration.toJsonSchema(type.payload, ctx.vendorPrefix);
|
|
1970
|
+
}
|
|
1971
|
+
function applyCustomConstraint(schema, constraint, ctx) {
|
|
1972
|
+
const registration = ctx.extensionRegistry?.findConstraint(constraint.constraintId);
|
|
1973
|
+
if (registration === void 0) {
|
|
1974
|
+
throw new Error(
|
|
1975
|
+
`Cannot generate JSON Schema for custom constraint "${constraint.constraintId}" without a matching extension registration`
|
|
1976
|
+
);
|
|
1977
|
+
}
|
|
1978
|
+
Object.assign(schema, registration.toJsonSchema(constraint.payload, ctx.vendorPrefix));
|
|
1979
|
+
}
|
|
1980
|
+
function applyCustomAnnotation(schema, annotation, ctx) {
|
|
1981
|
+
const registration = ctx.extensionRegistry?.findAnnotation(annotation.annotationId);
|
|
1982
|
+
if (registration === void 0) {
|
|
1983
|
+
throw new Error(
|
|
1984
|
+
`Cannot generate JSON Schema for custom annotation "${annotation.annotationId}" without a matching extension registration`
|
|
1985
|
+
);
|
|
1986
|
+
}
|
|
1987
|
+
if (registration.toJsonSchema === void 0) {
|
|
1988
|
+
return;
|
|
1989
|
+
}
|
|
1990
|
+
Object.assign(schema, registration.toJsonSchema(annotation.value, ctx.vendorPrefix));
|
|
1991
|
+
}
|
|
1532
1992
|
|
|
1533
1993
|
// src/ui-schema/schema.ts
|
|
1534
1994
|
import { z } from "zod";
|
|
@@ -1665,25 +2125,31 @@ function createShowRule(fieldName, value) {
|
|
|
1665
2125
|
}
|
|
1666
2126
|
};
|
|
1667
2127
|
}
|
|
2128
|
+
function flattenConditionSchema(scope, schema) {
|
|
2129
|
+
if (schema.allOf === void 0) {
|
|
2130
|
+
if (scope === "#") {
|
|
2131
|
+
return [schema];
|
|
2132
|
+
}
|
|
2133
|
+
const fieldName = scope.replace("#/properties/", "");
|
|
2134
|
+
return [
|
|
2135
|
+
{
|
|
2136
|
+
properties: {
|
|
2137
|
+
[fieldName]: schema
|
|
2138
|
+
}
|
|
2139
|
+
}
|
|
2140
|
+
];
|
|
2141
|
+
}
|
|
2142
|
+
return schema.allOf.flatMap((member) => flattenConditionSchema(scope, member));
|
|
2143
|
+
}
|
|
1668
2144
|
function combineRules(parentRule, childRule) {
|
|
1669
|
-
const parentCondition = parentRule.condition;
|
|
1670
|
-
const childCondition = childRule.condition;
|
|
1671
2145
|
return {
|
|
1672
2146
|
effect: "SHOW",
|
|
1673
2147
|
condition: {
|
|
1674
2148
|
scope: "#",
|
|
1675
2149
|
schema: {
|
|
1676
2150
|
allOf: [
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
[parentCondition.scope.replace("#/properties/", "")]: parentCondition.schema
|
|
1680
|
-
}
|
|
1681
|
-
},
|
|
1682
|
-
{
|
|
1683
|
-
properties: {
|
|
1684
|
-
[childCondition.scope.replace("#/properties/", "")]: childCondition.schema
|
|
1685
|
-
}
|
|
1686
|
-
}
|
|
2151
|
+
...flattenConditionSchema(parentRule.condition.scope, parentRule.condition.schema),
|
|
2152
|
+
...flattenConditionSchema(childRule.condition.scope, childRule.condition.schema)
|
|
1687
2153
|
]
|
|
1688
2154
|
}
|
|
1689
2155
|
}
|
|
@@ -1691,10 +2157,14 @@ function combineRules(parentRule, childRule) {
|
|
|
1691
2157
|
}
|
|
1692
2158
|
function fieldNodeToControl(field, parentRule) {
|
|
1693
2159
|
const displayNameAnnotation = field.annotations.find((a) => a.annotationKind === "displayName");
|
|
2160
|
+
const placeholderAnnotation = field.annotations.find((a) => a.annotationKind === "placeholder");
|
|
1694
2161
|
const control = {
|
|
1695
2162
|
type: "Control",
|
|
1696
2163
|
scope: fieldToScope(field.name),
|
|
1697
2164
|
...displayNameAnnotation !== void 0 && { label: displayNameAnnotation.value },
|
|
2165
|
+
...placeholderAnnotation !== void 0 && {
|
|
2166
|
+
options: { placeholder: placeholderAnnotation.value }
|
|
2167
|
+
},
|
|
1698
2168
|
...parentRule !== void 0 && { rule: parentRule }
|
|
1699
2169
|
};
|
|
1700
2170
|
return control;
|
|
@@ -1753,12 +2223,9 @@ function generateClassSchemas(analysis, source) {
|
|
|
1753
2223
|
}
|
|
1754
2224
|
|
|
1755
2225
|
// src/validate/constraint-validator.ts
|
|
1756
|
-
function makeCode(ctx, category, number) {
|
|
1757
|
-
return `${ctx.vendorPrefix}-${category}-${String(number).padStart(3, "0")}`;
|
|
1758
|
-
}
|
|
1759
2226
|
function addContradiction(ctx, message, primary, related) {
|
|
1760
2227
|
ctx.diagnostics.push({
|
|
1761
|
-
code:
|
|
2228
|
+
code: "CONTRADICTING_CONSTRAINTS",
|
|
1762
2229
|
message,
|
|
1763
2230
|
severity: "error",
|
|
1764
2231
|
primaryLocation: primary,
|
|
@@ -1767,7 +2234,7 @@ function addContradiction(ctx, message, primary, related) {
|
|
|
1767
2234
|
}
|
|
1768
2235
|
function addTypeMismatch(ctx, message, primary) {
|
|
1769
2236
|
ctx.diagnostics.push({
|
|
1770
|
-
code:
|
|
2237
|
+
code: "TYPE_MISMATCH",
|
|
1771
2238
|
message,
|
|
1772
2239
|
severity: "error",
|
|
1773
2240
|
primaryLocation: primary,
|
|
@@ -1776,13 +2243,31 @@ function addTypeMismatch(ctx, message, primary) {
|
|
|
1776
2243
|
}
|
|
1777
2244
|
function addUnknownExtension(ctx, message, primary) {
|
|
1778
2245
|
ctx.diagnostics.push({
|
|
1779
|
-
code:
|
|
2246
|
+
code: "UNKNOWN_EXTENSION",
|
|
1780
2247
|
message,
|
|
1781
2248
|
severity: "warning",
|
|
1782
2249
|
primaryLocation: primary,
|
|
1783
2250
|
relatedLocations: []
|
|
1784
2251
|
});
|
|
1785
2252
|
}
|
|
2253
|
+
function addUnknownPathTarget(ctx, message, primary) {
|
|
2254
|
+
ctx.diagnostics.push({
|
|
2255
|
+
code: "UNKNOWN_PATH_TARGET",
|
|
2256
|
+
message,
|
|
2257
|
+
severity: "error",
|
|
2258
|
+
primaryLocation: primary,
|
|
2259
|
+
relatedLocations: []
|
|
2260
|
+
});
|
|
2261
|
+
}
|
|
2262
|
+
function addConstraintBroadening(ctx, message, primary, related) {
|
|
2263
|
+
ctx.diagnostics.push({
|
|
2264
|
+
code: "CONSTRAINT_BROADENING",
|
|
2265
|
+
message,
|
|
2266
|
+
severity: "error",
|
|
2267
|
+
primaryLocation: primary,
|
|
2268
|
+
relatedLocations: [related]
|
|
2269
|
+
});
|
|
2270
|
+
}
|
|
1786
2271
|
function findNumeric(constraints, constraintKind) {
|
|
1787
2272
|
return constraints.find((c) => c.constraintKind === constraintKind);
|
|
1788
2273
|
}
|
|
@@ -1794,6 +2279,165 @@ function findAllowedMembers(constraints) {
|
|
|
1794
2279
|
(c) => c.constraintKind === "allowedMembers"
|
|
1795
2280
|
);
|
|
1796
2281
|
}
|
|
2282
|
+
function findConstConstraints(constraints) {
|
|
2283
|
+
return constraints.filter(
|
|
2284
|
+
(c) => c.constraintKind === "const"
|
|
2285
|
+
);
|
|
2286
|
+
}
|
|
2287
|
+
function jsonValueEquals(left, right) {
|
|
2288
|
+
if (left === right) {
|
|
2289
|
+
return true;
|
|
2290
|
+
}
|
|
2291
|
+
if (Array.isArray(left) || Array.isArray(right)) {
|
|
2292
|
+
if (!Array.isArray(left) || !Array.isArray(right) || left.length !== right.length) {
|
|
2293
|
+
return false;
|
|
2294
|
+
}
|
|
2295
|
+
return left.every((item, index) => jsonValueEquals(item, right[index]));
|
|
2296
|
+
}
|
|
2297
|
+
if (isJsonObject(left) || isJsonObject(right)) {
|
|
2298
|
+
if (!isJsonObject(left) || !isJsonObject(right)) {
|
|
2299
|
+
return false;
|
|
2300
|
+
}
|
|
2301
|
+
const leftKeys = Object.keys(left).sort();
|
|
2302
|
+
const rightKeys = Object.keys(right).sort();
|
|
2303
|
+
if (leftKeys.length !== rightKeys.length) {
|
|
2304
|
+
return false;
|
|
2305
|
+
}
|
|
2306
|
+
return leftKeys.every((key, index) => {
|
|
2307
|
+
const rightKey = rightKeys[index];
|
|
2308
|
+
if (rightKey !== key) {
|
|
2309
|
+
return false;
|
|
2310
|
+
}
|
|
2311
|
+
const leftValue = left[key];
|
|
2312
|
+
const rightValue = right[rightKey];
|
|
2313
|
+
return leftValue !== void 0 && rightValue !== void 0 && jsonValueEquals(leftValue, rightValue);
|
|
2314
|
+
});
|
|
2315
|
+
}
|
|
2316
|
+
return false;
|
|
2317
|
+
}
|
|
2318
|
+
function isJsonObject(value) {
|
|
2319
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2320
|
+
}
|
|
2321
|
+
function isOrderedBoundConstraint(constraint) {
|
|
2322
|
+
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";
|
|
2323
|
+
}
|
|
2324
|
+
function pathKey(constraint) {
|
|
2325
|
+
return constraint.path?.segments.join(".") ?? "";
|
|
2326
|
+
}
|
|
2327
|
+
function orderedBoundFamily(kind) {
|
|
2328
|
+
switch (kind) {
|
|
2329
|
+
case "minimum":
|
|
2330
|
+
case "exclusiveMinimum":
|
|
2331
|
+
return "numeric-lower";
|
|
2332
|
+
case "maximum":
|
|
2333
|
+
case "exclusiveMaximum":
|
|
2334
|
+
return "numeric-upper";
|
|
2335
|
+
case "minLength":
|
|
2336
|
+
return "minLength";
|
|
2337
|
+
case "minItems":
|
|
2338
|
+
return "minItems";
|
|
2339
|
+
case "maxLength":
|
|
2340
|
+
return "maxLength";
|
|
2341
|
+
case "maxItems":
|
|
2342
|
+
return "maxItems";
|
|
2343
|
+
default: {
|
|
2344
|
+
const _exhaustive = kind;
|
|
2345
|
+
return _exhaustive;
|
|
2346
|
+
}
|
|
2347
|
+
}
|
|
2348
|
+
}
|
|
2349
|
+
function isNumericLowerKind(kind) {
|
|
2350
|
+
return kind === "minimum" || kind === "exclusiveMinimum";
|
|
2351
|
+
}
|
|
2352
|
+
function isNumericUpperKind(kind) {
|
|
2353
|
+
return kind === "maximum" || kind === "exclusiveMaximum";
|
|
2354
|
+
}
|
|
2355
|
+
function describeConstraintTag(constraint) {
|
|
2356
|
+
return `@${constraint.constraintKind}`;
|
|
2357
|
+
}
|
|
2358
|
+
function compareConstraintStrength(current, previous) {
|
|
2359
|
+
const family = orderedBoundFamily(current.constraintKind);
|
|
2360
|
+
if (family === "numeric-lower") {
|
|
2361
|
+
if (!isNumericLowerKind(current.constraintKind) || !isNumericLowerKind(previous.constraintKind)) {
|
|
2362
|
+
throw new Error("numeric-lower family received non-numeric lower-bound constraint");
|
|
2363
|
+
}
|
|
2364
|
+
if (current.value !== previous.value) {
|
|
2365
|
+
return current.value > previous.value ? 1 : -1;
|
|
2366
|
+
}
|
|
2367
|
+
if (current.constraintKind === "exclusiveMinimum" && previous.constraintKind === "minimum") {
|
|
2368
|
+
return 1;
|
|
2369
|
+
}
|
|
2370
|
+
if (current.constraintKind === "minimum" && previous.constraintKind === "exclusiveMinimum") {
|
|
2371
|
+
return -1;
|
|
2372
|
+
}
|
|
2373
|
+
return 0;
|
|
2374
|
+
}
|
|
2375
|
+
if (family === "numeric-upper") {
|
|
2376
|
+
if (!isNumericUpperKind(current.constraintKind) || !isNumericUpperKind(previous.constraintKind)) {
|
|
2377
|
+
throw new Error("numeric-upper family received non-numeric upper-bound constraint");
|
|
2378
|
+
}
|
|
2379
|
+
if (current.value !== previous.value) {
|
|
2380
|
+
return current.value < previous.value ? 1 : -1;
|
|
2381
|
+
}
|
|
2382
|
+
if (current.constraintKind === "exclusiveMaximum" && previous.constraintKind === "maximum") {
|
|
2383
|
+
return 1;
|
|
2384
|
+
}
|
|
2385
|
+
if (current.constraintKind === "maximum" && previous.constraintKind === "exclusiveMaximum") {
|
|
2386
|
+
return -1;
|
|
2387
|
+
}
|
|
2388
|
+
return 0;
|
|
2389
|
+
}
|
|
2390
|
+
switch (family) {
|
|
2391
|
+
case "minLength":
|
|
2392
|
+
case "minItems":
|
|
2393
|
+
if (current.value === previous.value) {
|
|
2394
|
+
return 0;
|
|
2395
|
+
}
|
|
2396
|
+
return current.value > previous.value ? 1 : -1;
|
|
2397
|
+
case "maxLength":
|
|
2398
|
+
case "maxItems":
|
|
2399
|
+
if (current.value === previous.value) {
|
|
2400
|
+
return 0;
|
|
2401
|
+
}
|
|
2402
|
+
return current.value < previous.value ? 1 : -1;
|
|
2403
|
+
default: {
|
|
2404
|
+
const _exhaustive = family;
|
|
2405
|
+
return _exhaustive;
|
|
2406
|
+
}
|
|
2407
|
+
}
|
|
2408
|
+
}
|
|
2409
|
+
function checkConstraintBroadening(ctx, fieldName, constraints) {
|
|
2410
|
+
const strongestByKey = /* @__PURE__ */ new Map();
|
|
2411
|
+
for (const constraint of constraints) {
|
|
2412
|
+
if (!isOrderedBoundConstraint(constraint)) {
|
|
2413
|
+
continue;
|
|
2414
|
+
}
|
|
2415
|
+
const key = `${orderedBoundFamily(constraint.constraintKind)}:${pathKey(constraint)}`;
|
|
2416
|
+
const previous = strongestByKey.get(key);
|
|
2417
|
+
if (previous === void 0) {
|
|
2418
|
+
strongestByKey.set(key, constraint);
|
|
2419
|
+
continue;
|
|
2420
|
+
}
|
|
2421
|
+
const strength = compareConstraintStrength(constraint, previous);
|
|
2422
|
+
if (strength < 0) {
|
|
2423
|
+
const displayFieldName = formatPathTargetFieldName(
|
|
2424
|
+
fieldName,
|
|
2425
|
+
constraint.path?.segments ?? []
|
|
2426
|
+
);
|
|
2427
|
+
addConstraintBroadening(
|
|
2428
|
+
ctx,
|
|
2429
|
+
`Field "${displayFieldName}": ${describeConstraintTag(constraint)} (${String(constraint.value)}) is broader than earlier ${describeConstraintTag(previous)} (${String(previous.value)}). Constraints can only narrow.`,
|
|
2430
|
+
constraint.provenance,
|
|
2431
|
+
previous.provenance
|
|
2432
|
+
);
|
|
2433
|
+
continue;
|
|
2434
|
+
}
|
|
2435
|
+
if (strength <= 0) {
|
|
2436
|
+
continue;
|
|
2437
|
+
}
|
|
2438
|
+
strongestByKey.set(key, constraint);
|
|
2439
|
+
}
|
|
2440
|
+
}
|
|
1797
2441
|
function checkNumericContradictions(ctx, fieldName, constraints) {
|
|
1798
2442
|
const min = findNumeric(constraints, "minimum");
|
|
1799
2443
|
const max = findNumeric(constraints, "maximum");
|
|
@@ -1880,6 +2524,25 @@ function checkAllowedMembersContradiction(ctx, fieldName, constraints) {
|
|
|
1880
2524
|
}
|
|
1881
2525
|
}
|
|
1882
2526
|
}
|
|
2527
|
+
function checkConstContradictions(ctx, fieldName, constraints) {
|
|
2528
|
+
const constConstraints = findConstConstraints(constraints);
|
|
2529
|
+
if (constConstraints.length < 2) return;
|
|
2530
|
+
const first = constConstraints[0];
|
|
2531
|
+
if (first === void 0) return;
|
|
2532
|
+
for (let i = 1; i < constConstraints.length; i++) {
|
|
2533
|
+
const current = constConstraints[i];
|
|
2534
|
+
if (current === void 0) continue;
|
|
2535
|
+
if (jsonValueEquals(first.value, current.value)) {
|
|
2536
|
+
continue;
|
|
2537
|
+
}
|
|
2538
|
+
addContradiction(
|
|
2539
|
+
ctx,
|
|
2540
|
+
`Field "${fieldName}": conflicting @const constraints require both ${JSON.stringify(first.value)} and ${JSON.stringify(current.value)}`,
|
|
2541
|
+
first.provenance,
|
|
2542
|
+
current.provenance
|
|
2543
|
+
);
|
|
2544
|
+
}
|
|
2545
|
+
}
|
|
1883
2546
|
function typeLabel(type) {
|
|
1884
2547
|
switch (type.kind) {
|
|
1885
2548
|
case "primitive":
|
|
@@ -1890,6 +2553,8 @@ function typeLabel(type) {
|
|
|
1890
2553
|
return "array";
|
|
1891
2554
|
case "object":
|
|
1892
2555
|
return "object";
|
|
2556
|
+
case "record":
|
|
2557
|
+
return "record";
|
|
1893
2558
|
case "union":
|
|
1894
2559
|
return "union";
|
|
1895
2560
|
case "reference":
|
|
@@ -1904,85 +2569,173 @@ function typeLabel(type) {
|
|
|
1904
2569
|
}
|
|
1905
2570
|
}
|
|
1906
2571
|
}
|
|
1907
|
-
function
|
|
1908
|
-
|
|
1909
|
-
const
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
2572
|
+
function dereferenceType(ctx, type) {
|
|
2573
|
+
let current = type;
|
|
2574
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2575
|
+
while (current.kind === "reference") {
|
|
2576
|
+
if (seen.has(current.name)) {
|
|
2577
|
+
return current;
|
|
2578
|
+
}
|
|
2579
|
+
seen.add(current.name);
|
|
2580
|
+
const definition = ctx.typeRegistry[current.name];
|
|
2581
|
+
if (definition === void 0) {
|
|
2582
|
+
return current;
|
|
2583
|
+
}
|
|
2584
|
+
current = definition.type;
|
|
2585
|
+
}
|
|
2586
|
+
return current;
|
|
2587
|
+
}
|
|
2588
|
+
function resolvePathTargetType(ctx, type, segments) {
|
|
2589
|
+
const effectiveType = dereferenceType(ctx, type);
|
|
2590
|
+
if (segments.length === 0) {
|
|
2591
|
+
return { kind: "resolved", type: effectiveType };
|
|
2592
|
+
}
|
|
2593
|
+
if (effectiveType.kind === "array") {
|
|
2594
|
+
return resolvePathTargetType(ctx, effectiveType.items, segments);
|
|
2595
|
+
}
|
|
2596
|
+
if (effectiveType.kind === "object") {
|
|
2597
|
+
const [segment, ...rest] = segments;
|
|
2598
|
+
if (segment === void 0) {
|
|
2599
|
+
throw new Error("Invariant violation: object path traversal requires a segment");
|
|
2600
|
+
}
|
|
2601
|
+
const property = effectiveType.properties.find((prop) => prop.name === segment);
|
|
2602
|
+
if (property === void 0) {
|
|
2603
|
+
return { kind: "missing-property", segment };
|
|
2604
|
+
}
|
|
2605
|
+
return resolvePathTargetType(ctx, property.type, rest);
|
|
2606
|
+
}
|
|
2607
|
+
return { kind: "unresolvable", type: effectiveType };
|
|
2608
|
+
}
|
|
2609
|
+
function formatPathTargetFieldName(fieldName, path2) {
|
|
2610
|
+
return path2.length === 0 ? fieldName : `${fieldName}.${path2.join(".")}`;
|
|
2611
|
+
}
|
|
2612
|
+
function checkConstraintOnType(ctx, fieldName, type, constraint) {
|
|
2613
|
+
const effectiveType = dereferenceType(ctx, type);
|
|
2614
|
+
const isNumber = effectiveType.kind === "primitive" && effectiveType.primitiveKind === "number";
|
|
2615
|
+
const isString = effectiveType.kind === "primitive" && effectiveType.primitiveKind === "string";
|
|
2616
|
+
const isArray = effectiveType.kind === "array";
|
|
2617
|
+
const isEnum = effectiveType.kind === "enum";
|
|
2618
|
+
const arrayItemType = effectiveType.kind === "array" ? dereferenceType(ctx, effectiveType.items) : void 0;
|
|
2619
|
+
const isStringArray = arrayItemType?.kind === "primitive" && arrayItemType.primitiveKind === "string";
|
|
2620
|
+
const label = typeLabel(effectiveType);
|
|
2621
|
+
const ck = constraint.constraintKind;
|
|
2622
|
+
switch (ck) {
|
|
2623
|
+
case "minimum":
|
|
2624
|
+
case "maximum":
|
|
2625
|
+
case "exclusiveMinimum":
|
|
2626
|
+
case "exclusiveMaximum":
|
|
2627
|
+
case "multipleOf": {
|
|
2628
|
+
if (!isNumber) {
|
|
1917
2629
|
addTypeMismatch(
|
|
1918
2630
|
ctx,
|
|
1919
|
-
`Field "${fieldName}":
|
|
2631
|
+
`Field "${fieldName}": constraint "${ck}" is only valid on number fields, but field type is "${label}"`,
|
|
1920
2632
|
constraint.provenance
|
|
1921
2633
|
);
|
|
1922
2634
|
}
|
|
1923
|
-
|
|
2635
|
+
break;
|
|
1924
2636
|
}
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
ctx,
|
|
1935
|
-
`Field "${fieldName}": constraint "${ck}" is only valid on number fields, but field type is "${label}"`,
|
|
1936
|
-
constraint.provenance
|
|
1937
|
-
);
|
|
1938
|
-
}
|
|
1939
|
-
break;
|
|
2637
|
+
case "minLength":
|
|
2638
|
+
case "maxLength":
|
|
2639
|
+
case "pattern": {
|
|
2640
|
+
if (!isString && !isStringArray) {
|
|
2641
|
+
addTypeMismatch(
|
|
2642
|
+
ctx,
|
|
2643
|
+
`Field "${fieldName}": constraint "${ck}" is only valid on string fields or string array items, but field type is "${label}"`,
|
|
2644
|
+
constraint.provenance
|
|
2645
|
+
);
|
|
1940
2646
|
}
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
2647
|
+
break;
|
|
2648
|
+
}
|
|
2649
|
+
case "minItems":
|
|
2650
|
+
case "maxItems":
|
|
2651
|
+
case "uniqueItems": {
|
|
2652
|
+
if (!isArray) {
|
|
2653
|
+
addTypeMismatch(
|
|
2654
|
+
ctx,
|
|
2655
|
+
`Field "${fieldName}": constraint "${ck}" is only valid on array fields, but field type is "${label}"`,
|
|
2656
|
+
constraint.provenance
|
|
2657
|
+
);
|
|
1952
2658
|
}
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
|
|
2659
|
+
break;
|
|
2660
|
+
}
|
|
2661
|
+
case "allowedMembers": {
|
|
2662
|
+
if (!isEnum) {
|
|
2663
|
+
addTypeMismatch(
|
|
2664
|
+
ctx,
|
|
2665
|
+
`Field "${fieldName}": constraint "allowedMembers" is only valid on enum fields, but field type is "${label}"`,
|
|
2666
|
+
constraint.provenance
|
|
2667
|
+
);
|
|
2668
|
+
}
|
|
2669
|
+
break;
|
|
2670
|
+
}
|
|
2671
|
+
case "const": {
|
|
2672
|
+
const isPrimitiveConstType = effectiveType.kind === "primitive" && ["string", "number", "boolean", "null"].includes(effectiveType.primitiveKind) || effectiveType.kind === "enum";
|
|
2673
|
+
if (!isPrimitiveConstType) {
|
|
2674
|
+
addTypeMismatch(
|
|
2675
|
+
ctx,
|
|
2676
|
+
`Field "${fieldName}": constraint "const" is only valid on primitive or enum fields, but field type is "${label}"`,
|
|
2677
|
+
constraint.provenance
|
|
2678
|
+
);
|
|
1963
2679
|
break;
|
|
1964
2680
|
}
|
|
1965
|
-
|
|
1966
|
-
|
|
2681
|
+
if (effectiveType.kind === "primitive") {
|
|
2682
|
+
const valueType = constraint.value === null ? "null" : Array.isArray(constraint.value) ? "array" : typeof constraint.value;
|
|
2683
|
+
if (valueType !== effectiveType.primitiveKind) {
|
|
1967
2684
|
addTypeMismatch(
|
|
1968
2685
|
ctx,
|
|
1969
|
-
`Field "${fieldName}":
|
|
2686
|
+
`Field "${fieldName}": @const value type "${valueType}" is incompatible with field type "${effectiveType.primitiveKind}"`,
|
|
1970
2687
|
constraint.provenance
|
|
1971
2688
|
);
|
|
1972
2689
|
}
|
|
1973
2690
|
break;
|
|
1974
2691
|
}
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
|
|
2692
|
+
const memberValues = effectiveType.members.map((member) => member.value);
|
|
2693
|
+
if (!memberValues.some((member) => jsonValueEquals(member, constraint.value))) {
|
|
2694
|
+
addTypeMismatch(
|
|
2695
|
+
ctx,
|
|
2696
|
+
`Field "${fieldName}": @const value ${JSON.stringify(constraint.value)} is not one of the enum members`,
|
|
2697
|
+
constraint.provenance
|
|
2698
|
+
);
|
|
1978
2699
|
}
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
|
|
2700
|
+
break;
|
|
2701
|
+
}
|
|
2702
|
+
case "custom": {
|
|
2703
|
+
checkCustomConstraint(ctx, fieldName, effectiveType, constraint);
|
|
2704
|
+
break;
|
|
2705
|
+
}
|
|
2706
|
+
default: {
|
|
2707
|
+
const _exhaustive = constraint;
|
|
2708
|
+
throw new Error(
|
|
2709
|
+
`Unhandled constraint kind: ${_exhaustive.constraintKind}`
|
|
2710
|
+
);
|
|
2711
|
+
}
|
|
2712
|
+
}
|
|
2713
|
+
}
|
|
2714
|
+
function checkTypeApplicability(ctx, fieldName, type, constraints) {
|
|
2715
|
+
for (const constraint of constraints) {
|
|
2716
|
+
if (constraint.path) {
|
|
2717
|
+
const resolution = resolvePathTargetType(ctx, type, constraint.path.segments);
|
|
2718
|
+
const targetFieldName = formatPathTargetFieldName(fieldName, constraint.path.segments);
|
|
2719
|
+
if (resolution.kind === "missing-property") {
|
|
2720
|
+
addUnknownPathTarget(
|
|
2721
|
+
ctx,
|
|
2722
|
+
`Field "${targetFieldName}": path-targeted constraint "${constraint.constraintKind}" references unknown path segment "${resolution.segment}"`,
|
|
2723
|
+
constraint.provenance
|
|
1983
2724
|
);
|
|
2725
|
+
continue;
|
|
1984
2726
|
}
|
|
2727
|
+
if (resolution.kind === "unresolvable") {
|
|
2728
|
+
addTypeMismatch(
|
|
2729
|
+
ctx,
|
|
2730
|
+
`Field "${targetFieldName}": path-targeted constraint "${constraint.constraintKind}" is invalid because type "${typeLabel(resolution.type)}" cannot be traversed`,
|
|
2731
|
+
constraint.provenance
|
|
2732
|
+
);
|
|
2733
|
+
continue;
|
|
2734
|
+
}
|
|
2735
|
+
checkConstraintOnType(ctx, targetFieldName, resolution.type, constraint);
|
|
2736
|
+
continue;
|
|
1985
2737
|
}
|
|
2738
|
+
checkConstraintOnType(ctx, fieldName, type, constraint);
|
|
1986
2739
|
}
|
|
1987
2740
|
}
|
|
1988
2741
|
function checkCustomConstraint(ctx, fieldName, type, constraint) {
|
|
@@ -2026,6 +2779,8 @@ function validateConstraints(ctx, name, type, constraints) {
|
|
|
2026
2779
|
checkNumericContradictions(ctx, name, constraints);
|
|
2027
2780
|
checkLengthContradictions(ctx, name, constraints);
|
|
2028
2781
|
checkAllowedMembersContradiction(ctx, name, constraints);
|
|
2782
|
+
checkConstContradictions(ctx, name, constraints);
|
|
2783
|
+
checkConstraintBroadening(ctx, name, constraints);
|
|
2029
2784
|
checkTypeApplicability(ctx, name, type, constraints);
|
|
2030
2785
|
}
|
|
2031
2786
|
function validateElement(ctx, element) {
|
|
@@ -2052,8 +2807,8 @@ function validateElement(ctx, element) {
|
|
|
2052
2807
|
function validateIR(ir, options) {
|
|
2053
2808
|
const ctx = {
|
|
2054
2809
|
diagnostics: [],
|
|
2055
|
-
|
|
2056
|
-
|
|
2810
|
+
extensionRegistry: options?.extensionRegistry,
|
|
2811
|
+
typeRegistry: ir.typeRegistry
|
|
2057
2812
|
};
|
|
2058
2813
|
for (const element of ir.elements) {
|
|
2059
2814
|
validateElement(ctx, element);
|