@formspec/build 0.1.0-alpha.15 → 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.
Files changed (42) hide show
  1. package/dist/__tests__/fixtures/edge-cases.d.ts +11 -0
  2. package/dist/__tests__/fixtures/edge-cases.d.ts.map +1 -1
  3. package/dist/__tests__/fixtures/mixed-authoring-shipping-address.d.ts +30 -0
  4. package/dist/__tests__/fixtures/mixed-authoring-shipping-address.d.ts.map +1 -0
  5. package/dist/__tests__/mixed-authoring.test.d.ts +2 -0
  6. package/dist/__tests__/mixed-authoring.test.d.ts.map +1 -0
  7. package/dist/__tests__/parity/utils.d.ts +5 -3
  8. package/dist/__tests__/parity/utils.d.ts.map +1 -1
  9. package/dist/analyzer/class-analyzer.d.ts +4 -2
  10. package/dist/analyzer/class-analyzer.d.ts.map +1 -1
  11. package/dist/analyzer/tsdoc-parser.d.ts +20 -2
  12. package/dist/analyzer/tsdoc-parser.d.ts.map +1 -1
  13. package/dist/browser.cjs +172 -17
  14. package/dist/browser.cjs.map +1 -1
  15. package/dist/browser.d.ts.map +1 -1
  16. package/dist/browser.js +172 -17
  17. package/dist/browser.js.map +1 -1
  18. package/dist/build.d.ts +39 -1
  19. package/dist/canonicalize/tsdoc-canonicalizer.d.ts.map +1 -1
  20. package/dist/cli.cjs +634 -88
  21. package/dist/cli.cjs.map +1 -1
  22. package/dist/cli.js +634 -88
  23. package/dist/cli.js.map +1 -1
  24. package/dist/generators/mixed-authoring.d.ts +45 -0
  25. package/dist/generators/mixed-authoring.d.ts.map +1 -0
  26. package/dist/index.cjs +622 -87
  27. package/dist/index.cjs.map +1 -1
  28. package/dist/index.d.ts +2 -0
  29. package/dist/index.d.ts.map +1 -1
  30. package/dist/index.js +621 -87
  31. package/dist/index.js.map +1 -1
  32. package/dist/internals.cjs +526 -91
  33. package/dist/internals.cjs.map +1 -1
  34. package/dist/internals.js +526 -91
  35. package/dist/internals.js.map +1 -1
  36. package/dist/json-schema/ir-generator.d.ts +3 -2
  37. package/dist/json-schema/ir-generator.d.ts.map +1 -1
  38. package/dist/ui-schema/ir-generator.d.ts.map +1 -1
  39. package/dist/validate/constraint-validator.d.ts.map +1 -1
  40. package/package.json +3 -3
  41. package/dist/__tests__/jsdoc-constraints.test.d.ts +0 -9
  42. package/dist/__tests__/jsdoc-constraints.test.d.ts.map +0 -1
@@ -376,6 +376,7 @@ function canonicalizeTSDoc(analysis, source) {
376
376
  irVersion: import_core2.IR_VERSION,
377
377
  elements,
378
378
  typeRegistry: analysis.typeRegistry,
379
+ ...analysis.annotations !== void 0 && analysis.annotations.length > 0 && { annotations: analysis.annotations },
379
380
  provenance
380
381
  };
381
382
  }
@@ -540,7 +541,7 @@ var LENGTH_CONSTRAINT_MAP = {
540
541
  minItems: "minItems",
541
542
  maxItems: "maxItems"
542
543
  };
543
- var TAGS_REQUIRING_RAW_TEXT = /* @__PURE__ */ new Set(["pattern", "enumOptions"]);
544
+ var TAGS_REQUIRING_RAW_TEXT = /* @__PURE__ */ new Set(["pattern", "enumOptions", "defaultValue"]);
544
545
  function createFormSpecTSDocConfig() {
545
546
  const config = new import_tsdoc.TSDocConfiguration();
546
547
  for (const tagName of Object.keys(import_core3.BUILTIN_CONSTRAINT_DEFINITIONS)) {
@@ -552,7 +553,7 @@ function createFormSpecTSDocConfig() {
552
553
  })
553
554
  );
554
555
  }
555
- for (const tagName of ["displayName", "description"]) {
556
+ for (const tagName of ["displayName", "description", "format", "placeholder"]) {
556
557
  config.addTagDefinition(
557
558
  new import_tsdoc.TSDocTagDefinition({
558
559
  tagName: "@" + tagName,
@@ -571,6 +572,12 @@ function getParser() {
571
572
  function parseTSDocTags(node, file = "") {
572
573
  const constraints = [];
573
574
  const annotations = [];
575
+ let displayName;
576
+ let description;
577
+ let placeholder;
578
+ let displayNameProvenance;
579
+ let descriptionProvenance;
580
+ let placeholderProvenance;
574
581
  const sourceFile = node.getSourceFile();
575
582
  const sourceText = sourceFile.getFullText();
576
583
  const commentRanges = ts2.getLeadingCommentRanges(sourceText, node.getFullStart());
@@ -590,30 +597,37 @@ function parseTSDocTags(node, file = "") {
590
597
  const docComment = parserContext.docComment;
591
598
  for (const block of docComment.customBlocks) {
592
599
  const tagName = (0, import_core3.normalizeConstraintTagName)(block.blockTag.tagName.substring(1));
593
- if (tagName === "displayName" || tagName === "description") {
600
+ if (tagName === "displayName" || tagName === "description" || tagName === "format" || tagName === "placeholder") {
594
601
  const text2 = extractBlockText(block).trim();
595
602
  if (text2 === "") continue;
596
603
  const provenance2 = provenanceForComment(range, sourceFile, file, tagName);
597
604
  if (tagName === "displayName") {
605
+ if (!isMemberTargetDisplayName(text2) && displayName === void 0) {
606
+ displayName = text2;
607
+ displayNameProvenance = provenance2;
608
+ }
609
+ } else if (tagName === "format") {
598
610
  annotations.push({
599
611
  kind: "annotation",
600
- annotationKind: "displayName",
612
+ annotationKind: "format",
601
613
  value: text2,
602
614
  provenance: provenance2
603
615
  });
604
616
  } else {
605
- annotations.push({
606
- kind: "annotation",
607
- annotationKind: "description",
608
- value: text2,
609
- provenance: provenance2
610
- });
617
+ if (tagName === "description" && description === void 0) {
618
+ description = text2;
619
+ descriptionProvenance = provenance2;
620
+ } else if (tagName === "placeholder" && placeholder === void 0) {
621
+ placeholder = text2;
622
+ placeholderProvenance = provenance2;
623
+ }
611
624
  }
612
625
  continue;
613
626
  }
614
627
  if (TAGS_REQUIRING_RAW_TEXT.has(tagName)) continue;
615
628
  const text = extractBlockText(block).trim();
616
- if (text === "") continue;
629
+ const expectedType = (0, import_core3.isBuiltinConstraintName)(tagName) ? import_core3.BUILTIN_CONSTRAINT_DEFINITIONS[tagName] : void 0;
630
+ if (text === "" && expectedType !== "boolean") continue;
617
631
  const provenance = provenanceForComment(range, sourceFile, file, tagName);
618
632
  const constraintNode = parseConstraintValue(tagName, text, provenance);
619
633
  if (constraintNode) {
@@ -621,14 +635,47 @@ function parseTSDocTags(node, file = "") {
621
635
  }
622
636
  }
623
637
  if (docComment.deprecatedBlock !== void 0) {
638
+ const message = extractBlockText(docComment.deprecatedBlock).trim();
624
639
  annotations.push({
625
640
  kind: "annotation",
626
641
  annotationKind: "deprecated",
642
+ ...message !== "" && { message },
627
643
  provenance: provenanceForComment(range, sourceFile, file, "deprecated")
628
644
  });
629
645
  }
646
+ if (description === void 0 && docComment.remarksBlock !== void 0) {
647
+ const remarks = extractBlockText(docComment.remarksBlock).trim();
648
+ if (remarks !== "") {
649
+ description = remarks;
650
+ descriptionProvenance = provenanceForComment(range, sourceFile, file, "remarks");
651
+ }
652
+ }
630
653
  }
631
654
  }
655
+ if (displayName !== void 0 && displayNameProvenance !== void 0) {
656
+ annotations.push({
657
+ kind: "annotation",
658
+ annotationKind: "displayName",
659
+ value: displayName,
660
+ provenance: displayNameProvenance
661
+ });
662
+ }
663
+ if (description !== void 0 && descriptionProvenance !== void 0) {
664
+ annotations.push({
665
+ kind: "annotation",
666
+ annotationKind: "description",
667
+ value: description,
668
+ provenance: descriptionProvenance
669
+ });
670
+ }
671
+ if (placeholder !== void 0 && placeholderProvenance !== void 0) {
672
+ annotations.push({
673
+ kind: "annotation",
674
+ annotationKind: "placeholder",
675
+ value: placeholder,
676
+ provenance: placeholderProvenance
677
+ });
678
+ }
632
679
  const jsDocTagsAll = ts2.getJSDocTags(node);
633
680
  for (const tag of jsDocTagsAll) {
634
681
  const tagName = (0, import_core3.normalizeConstraintTagName)(tag.tagName.text);
@@ -637,6 +684,11 @@ function parseTSDocTags(node, file = "") {
637
684
  if (commentText === void 0 || commentText.trim() === "") continue;
638
685
  const text = commentText.trim();
639
686
  const provenance = provenanceForJSDocTag(tag, file);
687
+ if (tagName === "defaultValue") {
688
+ const defaultValueNode = parseDefaultValueValue(text, provenance);
689
+ annotations.push(defaultValueNode);
690
+ continue;
691
+ }
640
692
  const constraintNode = parseConstraintValue(tagName, text, provenance);
641
693
  if (constraintNode) {
642
694
  constraints.push(constraintNode);
@@ -644,6 +696,28 @@ function parseTSDocTags(node, file = "") {
644
696
  }
645
697
  return { constraints, annotations };
646
698
  }
699
+ function extractDisplayNameMetadata(node) {
700
+ let displayName;
701
+ const memberDisplayNames = /* @__PURE__ */ new Map();
702
+ for (const tag of ts2.getJSDocTags(node)) {
703
+ const tagName = (0, import_core3.normalizeConstraintTagName)(tag.tagName.text);
704
+ if (tagName !== "displayName") continue;
705
+ const commentText = getTagCommentText(tag);
706
+ if (commentText === void 0) continue;
707
+ const text = commentText.trim();
708
+ if (text === "") continue;
709
+ const memberTarget = parseMemberTargetDisplayName(text);
710
+ if (memberTarget) {
711
+ memberDisplayNames.set(memberTarget.target, memberTarget.label);
712
+ continue;
713
+ }
714
+ displayName ??= text;
715
+ }
716
+ return {
717
+ ...displayName !== void 0 && { displayName },
718
+ memberDisplayNames
719
+ };
720
+ }
647
721
  function extractPathTarget(text) {
648
722
  const trimmed = text.trimStart();
649
723
  const match = /^:([a-zA-Z_]\w*)\s+([\s\S]*)$/.exec(trimmed);
@@ -706,7 +780,45 @@ function parseConstraintValue(tagName, text, provenance) {
706
780
  }
707
781
  return null;
708
782
  }
783
+ if (expectedType === "boolean") {
784
+ const trimmed = effectiveText.trim();
785
+ if (trimmed !== "" && trimmed !== "true") {
786
+ return null;
787
+ }
788
+ if (tagName === "uniqueItems") {
789
+ return {
790
+ kind: "constraint",
791
+ constraintKind: "uniqueItems",
792
+ value: true,
793
+ ...path2 && { path: path2 },
794
+ provenance
795
+ };
796
+ }
797
+ return null;
798
+ }
709
799
  if (expectedType === "json") {
800
+ if (tagName === "const") {
801
+ const trimmedText = effectiveText.trim();
802
+ if (trimmedText === "") return null;
803
+ try {
804
+ const parsed2 = JSON.parse(trimmedText);
805
+ return {
806
+ kind: "constraint",
807
+ constraintKind: "const",
808
+ value: parsed2,
809
+ ...path2 && { path: path2 },
810
+ provenance
811
+ };
812
+ } catch {
813
+ return {
814
+ kind: "constraint",
815
+ constraintKind: "const",
816
+ value: trimmedText,
817
+ ...path2 && { path: path2 },
818
+ provenance
819
+ };
820
+ }
821
+ }
710
822
  const parsed = tryParseJson(effectiveText);
711
823
  if (!Array.isArray(parsed)) {
712
824
  return null;
@@ -738,6 +850,34 @@ function parseConstraintValue(tagName, text, provenance) {
738
850
  provenance
739
851
  };
740
852
  }
853
+ function parseDefaultValueValue(text, provenance) {
854
+ const trimmed = text.trim();
855
+ let value;
856
+ if (trimmed === "null") {
857
+ value = null;
858
+ } else if (trimmed === "true") {
859
+ value = true;
860
+ } else if (trimmed === "false") {
861
+ value = false;
862
+ } else {
863
+ const parsed = tryParseJson(trimmed);
864
+ value = parsed !== null ? parsed : trimmed;
865
+ }
866
+ return {
867
+ kind: "annotation",
868
+ annotationKind: "defaultValue",
869
+ value,
870
+ provenance
871
+ };
872
+ }
873
+ function isMemberTargetDisplayName(text) {
874
+ return parseMemberTargetDisplayName(text) !== null;
875
+ }
876
+ function parseMemberTargetDisplayName(text) {
877
+ const match = /^:([^\s]+)\s+([\s\S]+)$/.exec(text);
878
+ if (!match?.[1] || !match[2]) return null;
879
+ return { target: match[1], label: match[2].trim() };
880
+ }
741
881
  function provenanceForComment(range, sourceFile, file, tagName) {
742
882
  const { line, character } = sourceFile.getLineAndCharacterOfPosition(range.pos);
743
883
  return {
@@ -819,11 +959,17 @@ function isObjectType(type) {
819
959
  function isTypeReference(type) {
820
960
  return !!(type.flags & ts4.TypeFlags.Object) && !!(type.objectFlags & ts4.ObjectFlags.Reference);
821
961
  }
962
+ var RESOLVING_TYPE_PLACEHOLDER = {
963
+ kind: "object",
964
+ properties: [],
965
+ additionalProperties: true
966
+ };
822
967
  function analyzeClassToIR(classDecl, checker, file = "") {
823
968
  const name = classDecl.name?.text ?? "AnonymousClass";
824
969
  const fields = [];
825
970
  const fieldLayouts = [];
826
971
  const typeRegistry = {};
972
+ const annotations = extractJSDocAnnotationNodes(classDecl, file);
827
973
  const visiting = /* @__PURE__ */ new Set();
828
974
  const instanceMethods = [];
829
975
  const staticMethods = [];
@@ -846,12 +992,21 @@ function analyzeClassToIR(classDecl, checker, file = "") {
846
992
  }
847
993
  }
848
994
  }
849
- return { name, fields, fieldLayouts, typeRegistry, instanceMethods, staticMethods };
995
+ return {
996
+ name,
997
+ fields,
998
+ fieldLayouts,
999
+ typeRegistry,
1000
+ ...annotations.length > 0 && { annotations },
1001
+ instanceMethods,
1002
+ staticMethods
1003
+ };
850
1004
  }
851
1005
  function analyzeInterfaceToIR(interfaceDecl, checker, file = "") {
852
1006
  const name = interfaceDecl.name.text;
853
1007
  const fields = [];
854
1008
  const typeRegistry = {};
1009
+ const annotations = extractJSDocAnnotationNodes(interfaceDecl, file);
855
1010
  const visiting = /* @__PURE__ */ new Set();
856
1011
  for (const member of interfaceDecl.members) {
857
1012
  if (ts4.isPropertySignature(member)) {
@@ -862,7 +1017,15 @@ function analyzeInterfaceToIR(interfaceDecl, checker, file = "") {
862
1017
  }
863
1018
  }
864
1019
  const fieldLayouts = fields.map(() => ({}));
865
- return { name, fields, fieldLayouts, typeRegistry, instanceMethods: [], staticMethods: [] };
1020
+ return {
1021
+ name,
1022
+ fields,
1023
+ fieldLayouts,
1024
+ typeRegistry,
1025
+ ...annotations.length > 0 && { annotations },
1026
+ instanceMethods: [],
1027
+ staticMethods: []
1028
+ };
866
1029
  }
867
1030
  function analyzeTypeAliasToIR(typeAlias, checker, file = "") {
868
1031
  if (!ts4.isTypeLiteralNode(typeAlias.type)) {
@@ -877,6 +1040,7 @@ function analyzeTypeAliasToIR(typeAlias, checker, file = "") {
877
1040
  const name = typeAlias.name.text;
878
1041
  const fields = [];
879
1042
  const typeRegistry = {};
1043
+ const annotations = extractJSDocAnnotationNodes(typeAlias, file);
880
1044
  const visiting = /* @__PURE__ */ new Set();
881
1045
  for (const member of typeAlias.type.members) {
882
1046
  if (ts4.isPropertySignature(member)) {
@@ -893,6 +1057,7 @@ function analyzeTypeAliasToIR(typeAlias, checker, file = "") {
893
1057
  fields,
894
1058
  fieldLayouts: fields.map(() => ({})),
895
1059
  typeRegistry,
1060
+ ...annotations.length > 0 && { annotations },
896
1061
  instanceMethods: [],
897
1062
  staticMethods: []
898
1063
  }
@@ -906,7 +1071,7 @@ function analyzeFieldToIR(prop, checker, file, typeRegistry, visiting) {
906
1071
  const tsType = checker.getTypeAtLocation(prop);
907
1072
  const optional = prop.questionToken !== void 0;
908
1073
  const provenance = provenanceForNode(prop, file);
909
- let type = resolveTypeNode(tsType, checker, file, typeRegistry, visiting);
1074
+ let type = resolveTypeNode(tsType, checker, file, typeRegistry, visiting, prop);
910
1075
  const constraints = [];
911
1076
  if (prop.type) {
912
1077
  constraints.push(...extractTypeAliasConstraintNodes(prop.type, checker, file));
@@ -915,7 +1080,7 @@ function analyzeFieldToIR(prop, checker, file, typeRegistry, visiting) {
915
1080
  let annotations = [];
916
1081
  annotations.push(...extractJSDocAnnotationNodes(prop, file));
917
1082
  const defaultAnnotation = extractDefaultValueAnnotation(prop.initializer, file);
918
- if (defaultAnnotation) {
1083
+ if (defaultAnnotation && !annotations.some((a) => a.annotationKind === "defaultValue")) {
919
1084
  annotations.push(defaultAnnotation);
920
1085
  }
921
1086
  ({ type, annotations } = applyEnumMemberDisplayNames(type, annotations));
@@ -937,7 +1102,7 @@ function analyzeInterfacePropertyToIR(prop, checker, file, typeRegistry, visitin
937
1102
  const tsType = checker.getTypeAtLocation(prop);
938
1103
  const optional = prop.questionToken !== void 0;
939
1104
  const provenance = provenanceForNode(prop, file);
940
- let type = resolveTypeNode(tsType, checker, file, typeRegistry, visiting);
1105
+ let type = resolveTypeNode(tsType, checker, file, typeRegistry, visiting, prop);
941
1106
  const constraints = [];
942
1107
  if (prop.type) {
943
1108
  constraints.push(...extractTypeAliasConstraintNodes(prop.type, checker, file));
@@ -1018,7 +1183,7 @@ function parseEnumMemberDisplayName(value) {
1018
1183
  if (label === "") return null;
1019
1184
  return { value: match[1], label };
1020
1185
  }
1021
- function resolveTypeNode(type, checker, file, typeRegistry, visiting) {
1186
+ function resolveTypeNode(type, checker, file, typeRegistry, visiting, sourceNode) {
1022
1187
  if (type.flags & ts4.TypeFlags.String) {
1023
1188
  return { kind: "primitive", primitiveKind: "string" };
1024
1189
  }
@@ -1047,7 +1212,7 @@ function resolveTypeNode(type, checker, file, typeRegistry, visiting) {
1047
1212
  };
1048
1213
  }
1049
1214
  if (type.isUnion()) {
1050
- return resolveUnionType(type, checker, file, typeRegistry, visiting);
1215
+ return resolveUnionType(type, checker, file, typeRegistry, visiting, sourceNode);
1051
1216
  }
1052
1217
  if (checker.isArrayType(type)) {
1053
1218
  return resolveArrayType(type, checker, file, typeRegistry, visiting);
@@ -1057,70 +1222,102 @@ function resolveTypeNode(type, checker, file, typeRegistry, visiting) {
1057
1222
  }
1058
1223
  return { kind: "primitive", primitiveKind: "string" };
1059
1224
  }
1060
- function resolveUnionType(type, checker, file, typeRegistry, visiting) {
1225
+ function resolveUnionType(type, checker, file, typeRegistry, visiting, sourceNode) {
1226
+ const typeName = getNamedTypeName(type);
1227
+ const namedDecl = getNamedTypeDeclaration(type);
1228
+ if (typeName && typeName in typeRegistry) {
1229
+ return { kind: "reference", name: typeName, typeArguments: [] };
1230
+ }
1061
1231
  const allTypes = type.types;
1062
1232
  const nonNullTypes = allTypes.filter(
1063
1233
  (t) => !(t.flags & (ts4.TypeFlags.Null | ts4.TypeFlags.Undefined))
1064
1234
  );
1065
1235
  const hasNull = allTypes.some((t) => t.flags & ts4.TypeFlags.Null);
1236
+ const memberDisplayNames = /* @__PURE__ */ new Map();
1237
+ if (namedDecl) {
1238
+ for (const [value, label] of extractDisplayNameMetadata(namedDecl).memberDisplayNames) {
1239
+ memberDisplayNames.set(value, label);
1240
+ }
1241
+ }
1242
+ if (sourceNode) {
1243
+ for (const [value, label] of extractDisplayNameMetadata(sourceNode).memberDisplayNames) {
1244
+ memberDisplayNames.set(value, label);
1245
+ }
1246
+ }
1247
+ const registerNamed = (result) => {
1248
+ if (!typeName) {
1249
+ return result;
1250
+ }
1251
+ const annotations = namedDecl ? extractJSDocAnnotationNodes(namedDecl, file) : void 0;
1252
+ typeRegistry[typeName] = {
1253
+ name: typeName,
1254
+ type: result,
1255
+ ...annotations !== void 0 && annotations.length > 0 && { annotations },
1256
+ provenance: provenanceForDeclaration(namedDecl ?? sourceNode, file)
1257
+ };
1258
+ return { kind: "reference", name: typeName, typeArguments: [] };
1259
+ };
1260
+ const applyMemberLabels = (members2) => members2.map((value) => {
1261
+ const displayName = memberDisplayNames.get(String(value));
1262
+ return displayName !== void 0 ? { value, displayName } : { value };
1263
+ });
1066
1264
  const isBooleanUnion2 = nonNullTypes.length === 2 && nonNullTypes.every((t) => t.flags & ts4.TypeFlags.BooleanLiteral);
1067
1265
  if (isBooleanUnion2) {
1068
1266
  const boolNode = { kind: "primitive", primitiveKind: "boolean" };
1069
- if (hasNull) {
1070
- return {
1071
- kind: "union",
1072
- members: [boolNode, { kind: "primitive", primitiveKind: "null" }]
1073
- };
1074
- }
1075
- return boolNode;
1267
+ const result = hasNull ? {
1268
+ kind: "union",
1269
+ members: [boolNode, { kind: "primitive", primitiveKind: "null" }]
1270
+ } : boolNode;
1271
+ return registerNamed(result);
1076
1272
  }
1077
1273
  const allStringLiterals = nonNullTypes.every((t) => t.isStringLiteral());
1078
1274
  if (allStringLiterals && nonNullTypes.length > 0) {
1079
1275
  const stringTypes = nonNullTypes.filter((t) => t.isStringLiteral());
1080
1276
  const enumNode = {
1081
1277
  kind: "enum",
1082
- members: stringTypes.map((t) => ({ value: t.value }))
1278
+ members: applyMemberLabels(stringTypes.map((t) => t.value))
1083
1279
  };
1084
- if (hasNull) {
1085
- return {
1086
- kind: "union",
1087
- members: [enumNode, { kind: "primitive", primitiveKind: "null" }]
1088
- };
1089
- }
1090
- return enumNode;
1280
+ const result = hasNull ? {
1281
+ kind: "union",
1282
+ members: [enumNode, { kind: "primitive", primitiveKind: "null" }]
1283
+ } : enumNode;
1284
+ return registerNamed(result);
1091
1285
  }
1092
1286
  const allNumberLiterals = nonNullTypes.every((t) => t.isNumberLiteral());
1093
1287
  if (allNumberLiterals && nonNullTypes.length > 0) {
1094
1288
  const numberTypes = nonNullTypes.filter((t) => t.isNumberLiteral());
1095
1289
  const enumNode = {
1096
1290
  kind: "enum",
1097
- members: numberTypes.map((t) => ({ value: t.value }))
1291
+ members: applyMemberLabels(numberTypes.map((t) => t.value))
1098
1292
  };
1099
- if (hasNull) {
1100
- return {
1101
- kind: "union",
1102
- members: [enumNode, { kind: "primitive", primitiveKind: "null" }]
1103
- };
1104
- }
1105
- return enumNode;
1293
+ const result = hasNull ? {
1294
+ kind: "union",
1295
+ members: [enumNode, { kind: "primitive", primitiveKind: "null" }]
1296
+ } : enumNode;
1297
+ return registerNamed(result);
1106
1298
  }
1107
1299
  if (nonNullTypes.length === 1 && nonNullTypes[0]) {
1108
- const inner = resolveTypeNode(nonNullTypes[0], checker, file, typeRegistry, visiting);
1109
- if (hasNull) {
1110
- return {
1111
- kind: "union",
1112
- members: [inner, { kind: "primitive", primitiveKind: "null" }]
1113
- };
1114
- }
1115
- return inner;
1300
+ const inner = resolveTypeNode(
1301
+ nonNullTypes[0],
1302
+ checker,
1303
+ file,
1304
+ typeRegistry,
1305
+ visiting,
1306
+ sourceNode
1307
+ );
1308
+ const result = hasNull ? {
1309
+ kind: "union",
1310
+ members: [inner, { kind: "primitive", primitiveKind: "null" }]
1311
+ } : inner;
1312
+ return registerNamed(result);
1116
1313
  }
1117
1314
  const members = nonNullTypes.map(
1118
- (t) => resolveTypeNode(t, checker, file, typeRegistry, visiting)
1315
+ (t) => resolveTypeNode(t, checker, file, typeRegistry, visiting, sourceNode)
1119
1316
  );
1120
1317
  if (hasNull) {
1121
1318
  members.push({ kind: "primitive", primitiveKind: "null" });
1122
1319
  }
1123
- return { kind: "union", members };
1320
+ return registerNamed({ kind: "union", members });
1124
1321
  }
1125
1322
  function resolveArrayType(type, checker, file, typeRegistry, visiting) {
1126
1323
  const typeArgs = isTypeReference(type) ? type.typeArguments : void 0;
@@ -1136,30 +1333,84 @@ function tryResolveRecordType(type, checker, file, typeRegistry, visiting) {
1136
1333
  if (!indexInfo) {
1137
1334
  return null;
1138
1335
  }
1139
- if (visiting.has(type)) {
1140
- return null;
1141
- }
1142
- visiting.add(type);
1143
- try {
1144
- const valueType = resolveTypeNode(indexInfo.type, checker, file, typeRegistry, visiting);
1145
- return { kind: "record", valueType };
1146
- } finally {
1147
- visiting.delete(type);
1336
+ const valueType = resolveTypeNode(indexInfo.type, checker, file, typeRegistry, visiting);
1337
+ return { kind: "record", valueType };
1338
+ }
1339
+ function typeNodeContainsReference(type, targetName) {
1340
+ switch (type.kind) {
1341
+ case "reference":
1342
+ return type.name === targetName;
1343
+ case "array":
1344
+ return typeNodeContainsReference(type.items, targetName);
1345
+ case "record":
1346
+ return typeNodeContainsReference(type.valueType, targetName);
1347
+ case "union":
1348
+ return type.members.some((member) => typeNodeContainsReference(member, targetName));
1349
+ case "object":
1350
+ return type.properties.some(
1351
+ (property) => typeNodeContainsReference(property.type, targetName)
1352
+ );
1353
+ case "primitive":
1354
+ case "enum":
1355
+ case "dynamic":
1356
+ case "custom":
1357
+ return false;
1358
+ default: {
1359
+ const _exhaustive = type;
1360
+ return _exhaustive;
1361
+ }
1148
1362
  }
1149
1363
  }
1150
1364
  function resolveObjectType(type, checker, file, typeRegistry, visiting) {
1151
- const recordNode = tryResolveRecordType(type, checker, file, typeRegistry, visiting);
1152
- if (recordNode) {
1153
- return recordNode;
1154
- }
1365
+ const typeName = getNamedTypeName(type);
1366
+ const namedTypeName = typeName ?? void 0;
1367
+ const namedDecl = getNamedTypeDeclaration(type);
1368
+ const shouldRegisterNamedType = namedTypeName !== void 0 && !(namedTypeName === "Record" && namedDecl?.getSourceFile().fileName !== file);
1369
+ const clearNamedTypeRegistration = () => {
1370
+ if (namedTypeName === void 0 || !shouldRegisterNamedType) {
1371
+ return;
1372
+ }
1373
+ Reflect.deleteProperty(typeRegistry, namedTypeName);
1374
+ };
1155
1375
  if (visiting.has(type)) {
1376
+ if (namedTypeName !== void 0 && shouldRegisterNamedType) {
1377
+ return { kind: "reference", name: namedTypeName, typeArguments: [] };
1378
+ }
1156
1379
  return { kind: "object", properties: [], additionalProperties: false };
1157
1380
  }
1381
+ if (namedTypeName !== void 0 && shouldRegisterNamedType && !typeRegistry[namedTypeName]) {
1382
+ typeRegistry[namedTypeName] = {
1383
+ name: namedTypeName,
1384
+ type: RESOLVING_TYPE_PLACEHOLDER,
1385
+ provenance: provenanceForDeclaration(namedDecl, file)
1386
+ };
1387
+ }
1158
1388
  visiting.add(type);
1159
- const typeName = getNamedTypeName(type);
1160
- if (typeName && typeName in typeRegistry) {
1389
+ if (namedTypeName !== void 0 && shouldRegisterNamedType && typeRegistry[namedTypeName]?.type !== void 0) {
1390
+ if (typeRegistry[namedTypeName].type !== RESOLVING_TYPE_PLACEHOLDER) {
1391
+ visiting.delete(type);
1392
+ return { kind: "reference", name: namedTypeName, typeArguments: [] };
1393
+ }
1394
+ }
1395
+ const recordNode = tryResolveRecordType(type, checker, file, typeRegistry, visiting);
1396
+ if (recordNode) {
1161
1397
  visiting.delete(type);
1162
- return { kind: "reference", name: typeName, typeArguments: [] };
1398
+ if (namedTypeName !== void 0 && shouldRegisterNamedType) {
1399
+ const isRecursiveRecord = typeNodeContainsReference(recordNode.valueType, namedTypeName);
1400
+ if (!isRecursiveRecord) {
1401
+ clearNamedTypeRegistration();
1402
+ return recordNode;
1403
+ }
1404
+ const annotations = namedDecl ? extractJSDocAnnotationNodes(namedDecl, file) : void 0;
1405
+ typeRegistry[namedTypeName] = {
1406
+ name: namedTypeName,
1407
+ type: recordNode,
1408
+ ...annotations !== void 0 && annotations.length > 0 && { annotations },
1409
+ provenance: provenanceForDeclaration(namedDecl, file)
1410
+ };
1411
+ return { kind: "reference", name: namedTypeName, typeArguments: [] };
1412
+ }
1413
+ return recordNode;
1163
1414
  }
1164
1415
  const properties = [];
1165
1416
  const fieldInfoMap = getNamedTypeFieldNodeInfoMap(type, checker, file, typeRegistry, visiting);
@@ -1168,7 +1419,14 @@ function resolveObjectType(type, checker, file, typeRegistry, visiting) {
1168
1419
  if (!declaration) continue;
1169
1420
  const propType = checker.getTypeOfSymbolAtLocation(prop, declaration);
1170
1421
  const optional = !!(prop.flags & ts4.SymbolFlags.Optional);
1171
- const propTypeNode = resolveTypeNode(propType, checker, file, typeRegistry, visiting);
1422
+ const propTypeNode = resolveTypeNode(
1423
+ propType,
1424
+ checker,
1425
+ file,
1426
+ typeRegistry,
1427
+ visiting,
1428
+ declaration
1429
+ );
1172
1430
  const fieldNodeInfo = fieldInfoMap?.get(prop.name);
1173
1431
  properties.push({
1174
1432
  name: prop.name,
@@ -1185,13 +1443,15 @@ function resolveObjectType(type, checker, file, typeRegistry, visiting) {
1185
1443
  properties,
1186
1444
  additionalProperties: true
1187
1445
  };
1188
- if (typeName) {
1189
- typeRegistry[typeName] = {
1190
- name: typeName,
1446
+ if (namedTypeName !== void 0 && shouldRegisterNamedType) {
1447
+ const annotations = namedDecl ? extractJSDocAnnotationNodes(namedDecl, file) : void 0;
1448
+ typeRegistry[namedTypeName] = {
1449
+ name: namedTypeName,
1191
1450
  type: objectNode,
1192
- provenance: provenanceForFile(file)
1451
+ ...annotations !== void 0 && annotations.length > 0 && { annotations },
1452
+ provenance: provenanceForDeclaration(namedDecl, file)
1193
1453
  };
1194
- return { kind: "reference", name: typeName, typeArguments: [] };
1454
+ return { kind: "reference", name: namedTypeName, typeArguments: [] };
1195
1455
  }
1196
1456
  return objectNode;
1197
1457
  }
@@ -1283,6 +1543,12 @@ function provenanceForNode(node, file) {
1283
1543
  function provenanceForFile(file) {
1284
1544
  return { surface: "tsdoc", file, line: 0, column: 0 };
1285
1545
  }
1546
+ function provenanceForDeclaration(node, file) {
1547
+ if (!node) {
1548
+ return provenanceForFile(file);
1549
+ }
1550
+ return provenanceForNode(node, file);
1551
+ }
1286
1552
  function getNamedTypeName(type) {
1287
1553
  const symbol = type.getSymbol();
1288
1554
  if (symbol?.declarations) {
@@ -1301,6 +1567,20 @@ function getNamedTypeName(type) {
1301
1567
  }
1302
1568
  return null;
1303
1569
  }
1570
+ function getNamedTypeDeclaration(type) {
1571
+ const symbol = type.getSymbol();
1572
+ if (symbol?.declarations) {
1573
+ const decl = symbol.declarations[0];
1574
+ if (decl && (ts4.isClassDeclaration(decl) || ts4.isInterfaceDeclaration(decl) || ts4.isTypeAliasDeclaration(decl))) {
1575
+ return decl;
1576
+ }
1577
+ }
1578
+ const aliasSymbol = type.aliasSymbol;
1579
+ if (aliasSymbol?.declarations) {
1580
+ return aliasSymbol.declarations.find(ts4.isTypeAliasDeclaration);
1581
+ }
1582
+ return void 0;
1583
+ }
1304
1584
  function analyzeMethod(method, checker) {
1305
1585
  if (!ts4.isIdentifier(method.name)) {
1306
1586
  return null;
@@ -1360,6 +1640,9 @@ function generateJsonSchemaFromIR(ir, options) {
1360
1640
  const ctx = makeContext(options);
1361
1641
  for (const [name, typeDef] of Object.entries(ir.typeRegistry)) {
1362
1642
  ctx.defs[name] = generateTypeNode(typeDef.type, ctx);
1643
+ if (typeDef.annotations && typeDef.annotations.length > 0) {
1644
+ applyAnnotations(ctx.defs[name], typeDef.annotations, ctx);
1645
+ }
1363
1646
  }
1364
1647
  const properties = {};
1365
1648
  const required = [];
@@ -1371,6 +1654,9 @@ function generateJsonSchemaFromIR(ir, options) {
1371
1654
  properties,
1372
1655
  ...uniqueRequired.length > 0 && { required: uniqueRequired }
1373
1656
  };
1657
+ if (ir.annotations && ir.annotations.length > 0) {
1658
+ applyAnnotations(result, ir.annotations, ctx);
1659
+ }
1374
1660
  if (Object.keys(ctx.defs).length > 0) {
1375
1661
  result.$defs = ctx.defs;
1376
1662
  }
@@ -1400,22 +1686,51 @@ function collectFields(elements, properties, required, ctx) {
1400
1686
  }
1401
1687
  function generateFieldSchema(field, ctx) {
1402
1688
  const schema = generateTypeNode(field.type, ctx);
1689
+ const itemStringSchema = schema.type === "array" && schema.items?.type === "string" ? schema.items : void 0;
1403
1690
  const directConstraints = [];
1691
+ const itemConstraints = [];
1404
1692
  const pathConstraints = [];
1405
1693
  for (const c of field.constraints) {
1406
1694
  if (c.path) {
1407
1695
  pathConstraints.push(c);
1696
+ } else if (itemStringSchema !== void 0 && isStringItemConstraint(c)) {
1697
+ itemConstraints.push(c);
1408
1698
  } else {
1409
1699
  directConstraints.push(c);
1410
1700
  }
1411
1701
  }
1412
1702
  applyConstraints(schema, directConstraints, ctx);
1413
- applyAnnotations(schema, field.annotations, ctx);
1703
+ if (itemStringSchema !== void 0) {
1704
+ applyConstraints(itemStringSchema, itemConstraints, ctx);
1705
+ }
1706
+ const rootAnnotations = [];
1707
+ const itemAnnotations = [];
1708
+ for (const annotation of field.annotations) {
1709
+ if (itemStringSchema !== void 0 && annotation.annotationKind === "format") {
1710
+ itemAnnotations.push(annotation);
1711
+ } else {
1712
+ rootAnnotations.push(annotation);
1713
+ }
1714
+ }
1715
+ applyAnnotations(schema, rootAnnotations, ctx);
1716
+ if (itemStringSchema !== void 0) {
1717
+ applyAnnotations(itemStringSchema, itemAnnotations, ctx);
1718
+ }
1414
1719
  if (pathConstraints.length === 0) {
1415
1720
  return schema;
1416
1721
  }
1417
1722
  return applyPathTargetedConstraints(schema, pathConstraints, ctx);
1418
1723
  }
1724
+ function isStringItemConstraint(constraint) {
1725
+ switch (constraint.constraintKind) {
1726
+ case "minLength":
1727
+ case "maxLength":
1728
+ case "pattern":
1729
+ return true;
1730
+ default:
1731
+ return false;
1732
+ }
1733
+ }
1419
1734
  function applyPathTargetedConstraints(schema, pathConstraints, ctx) {
1420
1735
  if (schema.type === "array" && schema.items) {
1421
1736
  schema.items = applyPathTargetedConstraints(schema.items, pathConstraints, ctx);
@@ -1633,6 +1948,9 @@ function applyConstraints(schema, constraints, ctx) {
1633
1948
  case "uniqueItems":
1634
1949
  schema.uniqueItems = constraint.value;
1635
1950
  break;
1951
+ case "const":
1952
+ schema.const = constraint.value;
1953
+ break;
1636
1954
  case "allowedMembers":
1637
1955
  break;
1638
1956
  case "custom":
@@ -1657,8 +1975,14 @@ function applyAnnotations(schema, annotations, ctx) {
1657
1975
  case "defaultValue":
1658
1976
  schema.default = annotation.value;
1659
1977
  break;
1978
+ case "format":
1979
+ schema.format = annotation.value;
1980
+ break;
1660
1981
  case "deprecated":
1661
1982
  schema.deprecated = true;
1983
+ if (annotation.message !== void 0 && annotation.message !== "") {
1984
+ schema["x-formspec-deprecation-description"] = annotation.message;
1985
+ }
1662
1986
  break;
1663
1987
  case "placeholder":
1664
1988
  break;
@@ -1840,25 +2164,31 @@ function createShowRule(fieldName, value) {
1840
2164
  }
1841
2165
  };
1842
2166
  }
2167
+ function flattenConditionSchema(scope, schema) {
2168
+ if (schema.allOf === void 0) {
2169
+ if (scope === "#") {
2170
+ return [schema];
2171
+ }
2172
+ const fieldName = scope.replace("#/properties/", "");
2173
+ return [
2174
+ {
2175
+ properties: {
2176
+ [fieldName]: schema
2177
+ }
2178
+ }
2179
+ ];
2180
+ }
2181
+ return schema.allOf.flatMap((member) => flattenConditionSchema(scope, member));
2182
+ }
1843
2183
  function combineRules(parentRule, childRule) {
1844
- const parentCondition = parentRule.condition;
1845
- const childCondition = childRule.condition;
1846
2184
  return {
1847
2185
  effect: "SHOW",
1848
2186
  condition: {
1849
2187
  scope: "#",
1850
2188
  schema: {
1851
2189
  allOf: [
1852
- {
1853
- properties: {
1854
- [parentCondition.scope.replace("#/properties/", "")]: parentCondition.schema
1855
- }
1856
- },
1857
- {
1858
- properties: {
1859
- [childCondition.scope.replace("#/properties/", "")]: childCondition.schema
1860
- }
1861
- }
2190
+ ...flattenConditionSchema(parentRule.condition.scope, parentRule.condition.schema),
2191
+ ...flattenConditionSchema(childRule.condition.scope, childRule.condition.schema)
1862
2192
  ]
1863
2193
  }
1864
2194
  }
@@ -1866,10 +2196,14 @@ function combineRules(parentRule, childRule) {
1866
2196
  }
1867
2197
  function fieldNodeToControl(field, parentRule) {
1868
2198
  const displayNameAnnotation = field.annotations.find((a) => a.annotationKind === "displayName");
2199
+ const placeholderAnnotation = field.annotations.find((a) => a.annotationKind === "placeholder");
1869
2200
  const control = {
1870
2201
  type: "Control",
1871
2202
  scope: fieldToScope(field.name),
1872
2203
  ...displayNameAnnotation !== void 0 && { label: displayNameAnnotation.value },
2204
+ ...placeholderAnnotation !== void 0 && {
2205
+ options: { placeholder: placeholderAnnotation.value }
2206
+ },
1873
2207
  ...parentRule !== void 0 && { rule: parentRule }
1874
2208
  };
1875
2209
  return control;
@@ -1955,6 +2289,15 @@ function addUnknownExtension(ctx, message, primary) {
1955
2289
  relatedLocations: []
1956
2290
  });
1957
2291
  }
2292
+ function addUnknownPathTarget(ctx, message, primary) {
2293
+ ctx.diagnostics.push({
2294
+ code: "UNKNOWN_PATH_TARGET",
2295
+ message,
2296
+ severity: "error",
2297
+ primaryLocation: primary,
2298
+ relatedLocations: []
2299
+ });
2300
+ }
1958
2301
  function addConstraintBroadening(ctx, message, primary, related) {
1959
2302
  ctx.diagnostics.push({
1960
2303
  code: "CONSTRAINT_BROADENING",
@@ -1975,6 +2318,45 @@ function findAllowedMembers(constraints) {
1975
2318
  (c) => c.constraintKind === "allowedMembers"
1976
2319
  );
1977
2320
  }
2321
+ function findConstConstraints(constraints) {
2322
+ return constraints.filter(
2323
+ (c) => c.constraintKind === "const"
2324
+ );
2325
+ }
2326
+ function jsonValueEquals(left, right) {
2327
+ if (left === right) {
2328
+ return true;
2329
+ }
2330
+ if (Array.isArray(left) || Array.isArray(right)) {
2331
+ if (!Array.isArray(left) || !Array.isArray(right) || left.length !== right.length) {
2332
+ return false;
2333
+ }
2334
+ return left.every((item, index) => jsonValueEquals(item, right[index]));
2335
+ }
2336
+ if (isJsonObject(left) || isJsonObject(right)) {
2337
+ if (!isJsonObject(left) || !isJsonObject(right)) {
2338
+ return false;
2339
+ }
2340
+ const leftKeys = Object.keys(left).sort();
2341
+ const rightKeys = Object.keys(right).sort();
2342
+ if (leftKeys.length !== rightKeys.length) {
2343
+ return false;
2344
+ }
2345
+ return leftKeys.every((key, index) => {
2346
+ const rightKey = rightKeys[index];
2347
+ if (rightKey !== key) {
2348
+ return false;
2349
+ }
2350
+ const leftValue = left[key];
2351
+ const rightValue = right[rightKey];
2352
+ return leftValue !== void 0 && rightValue !== void 0 && jsonValueEquals(leftValue, rightValue);
2353
+ });
2354
+ }
2355
+ return false;
2356
+ }
2357
+ function isJsonObject(value) {
2358
+ return typeof value === "object" && value !== null && !Array.isArray(value);
2359
+ }
1978
2360
  function isOrderedBoundConstraint(constraint) {
1979
2361
  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";
1980
2362
  }
@@ -2181,6 +2563,25 @@ function checkAllowedMembersContradiction(ctx, fieldName, constraints) {
2181
2563
  }
2182
2564
  }
2183
2565
  }
2566
+ function checkConstContradictions(ctx, fieldName, constraints) {
2567
+ const constConstraints = findConstConstraints(constraints);
2568
+ if (constConstraints.length < 2) return;
2569
+ const first = constConstraints[0];
2570
+ if (first === void 0) return;
2571
+ for (let i = 1; i < constConstraints.length; i++) {
2572
+ const current = constConstraints[i];
2573
+ if (current === void 0) continue;
2574
+ if (jsonValueEquals(first.value, current.value)) {
2575
+ continue;
2576
+ }
2577
+ addContradiction(
2578
+ ctx,
2579
+ `Field "${fieldName}": conflicting @const constraints require both ${JSON.stringify(first.value)} and ${JSON.stringify(current.value)}`,
2580
+ first.provenance,
2581
+ current.provenance
2582
+ );
2583
+ }
2584
+ }
2184
2585
  function typeLabel(type) {
2185
2586
  switch (type.kind) {
2186
2587
  case "primitive":
@@ -2253,6 +2654,8 @@ function checkConstraintOnType(ctx, fieldName, type, constraint) {
2253
2654
  const isString = effectiveType.kind === "primitive" && effectiveType.primitiveKind === "string";
2254
2655
  const isArray = effectiveType.kind === "array";
2255
2656
  const isEnum = effectiveType.kind === "enum";
2657
+ const arrayItemType = effectiveType.kind === "array" ? dereferenceType(ctx, effectiveType.items) : void 0;
2658
+ const isStringArray = arrayItemType?.kind === "primitive" && arrayItemType.primitiveKind === "string";
2256
2659
  const label = typeLabel(effectiveType);
2257
2660
  const ck = constraint.constraintKind;
2258
2661
  switch (ck) {
@@ -2273,10 +2676,10 @@ function checkConstraintOnType(ctx, fieldName, type, constraint) {
2273
2676
  case "minLength":
2274
2677
  case "maxLength":
2275
2678
  case "pattern": {
2276
- if (!isString) {
2679
+ if (!isString && !isStringArray) {
2277
2680
  addTypeMismatch(
2278
2681
  ctx,
2279
- `Field "${fieldName}": constraint "${ck}" is only valid on string fields, but field type is "${label}"`,
2682
+ `Field "${fieldName}": constraint "${ck}" is only valid on string fields or string array items, but field type is "${label}"`,
2280
2683
  constraint.provenance
2281
2684
  );
2282
2685
  }
@@ -2304,6 +2707,37 @@ function checkConstraintOnType(ctx, fieldName, type, constraint) {
2304
2707
  }
2305
2708
  break;
2306
2709
  }
2710
+ case "const": {
2711
+ const isPrimitiveConstType = effectiveType.kind === "primitive" && ["string", "number", "boolean", "null"].includes(effectiveType.primitiveKind) || effectiveType.kind === "enum";
2712
+ if (!isPrimitiveConstType) {
2713
+ addTypeMismatch(
2714
+ ctx,
2715
+ `Field "${fieldName}": constraint "const" is only valid on primitive or enum fields, but field type is "${label}"`,
2716
+ constraint.provenance
2717
+ );
2718
+ break;
2719
+ }
2720
+ if (effectiveType.kind === "primitive") {
2721
+ const valueType = constraint.value === null ? "null" : Array.isArray(constraint.value) ? "array" : typeof constraint.value;
2722
+ if (valueType !== effectiveType.primitiveKind) {
2723
+ addTypeMismatch(
2724
+ ctx,
2725
+ `Field "${fieldName}": @const value type "${valueType}" is incompatible with field type "${effectiveType.primitiveKind}"`,
2726
+ constraint.provenance
2727
+ );
2728
+ }
2729
+ break;
2730
+ }
2731
+ const memberValues = effectiveType.members.map((member) => member.value);
2732
+ if (!memberValues.some((member) => jsonValueEquals(member, constraint.value))) {
2733
+ addTypeMismatch(
2734
+ ctx,
2735
+ `Field "${fieldName}": @const value ${JSON.stringify(constraint.value)} is not one of the enum members`,
2736
+ constraint.provenance
2737
+ );
2738
+ }
2739
+ break;
2740
+ }
2307
2741
  case "custom": {
2308
2742
  checkCustomConstraint(ctx, fieldName, effectiveType, constraint);
2309
2743
  break;
@@ -2322,9 +2756,9 @@ function checkTypeApplicability(ctx, fieldName, type, constraints) {
2322
2756
  const resolution = resolvePathTargetType(ctx, type, constraint.path.segments);
2323
2757
  const targetFieldName = formatPathTargetFieldName(fieldName, constraint.path.segments);
2324
2758
  if (resolution.kind === "missing-property") {
2325
- addTypeMismatch(
2759
+ addUnknownPathTarget(
2326
2760
  ctx,
2327
- `Field "${fieldName}": path-targeted constraint "${constraint.constraintKind}" references unknown path segment "${resolution.segment}"`,
2761
+ `Field "${targetFieldName}": path-targeted constraint "${constraint.constraintKind}" references unknown path segment "${resolution.segment}"`,
2328
2762
  constraint.provenance
2329
2763
  );
2330
2764
  continue;
@@ -2384,6 +2818,7 @@ function validateConstraints(ctx, name, type, constraints) {
2384
2818
  checkNumericContradictions(ctx, name, constraints);
2385
2819
  checkLengthContradictions(ctx, name, constraints);
2386
2820
  checkAllowedMembersContradiction(ctx, name, constraints);
2821
+ checkConstContradictions(ctx, name, constraints);
2387
2822
  checkConstraintBroadening(ctx, name, constraints);
2388
2823
  checkTypeApplicability(ctx, name, type, constraints);
2389
2824
  }