@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
package/dist/internals.js CHANGED
@@ -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
  }
@@ -501,7 +502,7 @@ var LENGTH_CONSTRAINT_MAP = {
501
502
  minItems: "minItems",
502
503
  maxItems: "maxItems"
503
504
  };
504
- var TAGS_REQUIRING_RAW_TEXT = /* @__PURE__ */ new Set(["pattern", "enumOptions"]);
505
+ var TAGS_REQUIRING_RAW_TEXT = /* @__PURE__ */ new Set(["pattern", "enumOptions", "defaultValue"]);
505
506
  function createFormSpecTSDocConfig() {
506
507
  const config = new TSDocConfiguration();
507
508
  for (const tagName of Object.keys(BUILTIN_CONSTRAINT_DEFINITIONS)) {
@@ -513,7 +514,7 @@ function createFormSpecTSDocConfig() {
513
514
  })
514
515
  );
515
516
  }
516
- for (const tagName of ["displayName", "description"]) {
517
+ for (const tagName of ["displayName", "description", "format", "placeholder"]) {
517
518
  config.addTagDefinition(
518
519
  new TSDocTagDefinition({
519
520
  tagName: "@" + tagName,
@@ -532,6 +533,12 @@ function getParser() {
532
533
  function parseTSDocTags(node, file = "") {
533
534
  const constraints = [];
534
535
  const annotations = [];
536
+ let displayName;
537
+ let description;
538
+ let placeholder;
539
+ let displayNameProvenance;
540
+ let descriptionProvenance;
541
+ let placeholderProvenance;
535
542
  const sourceFile = node.getSourceFile();
536
543
  const sourceText = sourceFile.getFullText();
537
544
  const commentRanges = ts2.getLeadingCommentRanges(sourceText, node.getFullStart());
@@ -551,30 +558,37 @@ function parseTSDocTags(node, file = "") {
551
558
  const docComment = parserContext.docComment;
552
559
  for (const block of docComment.customBlocks) {
553
560
  const tagName = normalizeConstraintTagName(block.blockTag.tagName.substring(1));
554
- if (tagName === "displayName" || tagName === "description") {
561
+ if (tagName === "displayName" || tagName === "description" || tagName === "format" || tagName === "placeholder") {
555
562
  const text2 = extractBlockText(block).trim();
556
563
  if (text2 === "") continue;
557
564
  const provenance2 = provenanceForComment(range, sourceFile, file, tagName);
558
565
  if (tagName === "displayName") {
566
+ if (!isMemberTargetDisplayName(text2) && displayName === void 0) {
567
+ displayName = text2;
568
+ displayNameProvenance = provenance2;
569
+ }
570
+ } else if (tagName === "format") {
559
571
  annotations.push({
560
572
  kind: "annotation",
561
- annotationKind: "displayName",
573
+ annotationKind: "format",
562
574
  value: text2,
563
575
  provenance: provenance2
564
576
  });
565
577
  } else {
566
- annotations.push({
567
- kind: "annotation",
568
- annotationKind: "description",
569
- value: text2,
570
- provenance: provenance2
571
- });
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
+ }
572
585
  }
573
586
  continue;
574
587
  }
575
588
  if (TAGS_REQUIRING_RAW_TEXT.has(tagName)) continue;
576
589
  const text = extractBlockText(block).trim();
577
- if (text === "") continue;
590
+ const expectedType = isBuiltinConstraintName(tagName) ? BUILTIN_CONSTRAINT_DEFINITIONS[tagName] : void 0;
591
+ if (text === "" && expectedType !== "boolean") continue;
578
592
  const provenance = provenanceForComment(range, sourceFile, file, tagName);
579
593
  const constraintNode = parseConstraintValue(tagName, text, provenance);
580
594
  if (constraintNode) {
@@ -582,14 +596,47 @@ function parseTSDocTags(node, file = "") {
582
596
  }
583
597
  }
584
598
  if (docComment.deprecatedBlock !== void 0) {
599
+ const message = extractBlockText(docComment.deprecatedBlock).trim();
585
600
  annotations.push({
586
601
  kind: "annotation",
587
602
  annotationKind: "deprecated",
603
+ ...message !== "" && { message },
588
604
  provenance: provenanceForComment(range, sourceFile, file, "deprecated")
589
605
  });
590
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
+ }
591
614
  }
592
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
+ }
593
640
  const jsDocTagsAll = ts2.getJSDocTags(node);
594
641
  for (const tag of jsDocTagsAll) {
595
642
  const tagName = normalizeConstraintTagName(tag.tagName.text);
@@ -598,6 +645,11 @@ function parseTSDocTags(node, file = "") {
598
645
  if (commentText === void 0 || commentText.trim() === "") continue;
599
646
  const text = commentText.trim();
600
647
  const provenance = provenanceForJSDocTag(tag, file);
648
+ if (tagName === "defaultValue") {
649
+ const defaultValueNode = parseDefaultValueValue(text, provenance);
650
+ annotations.push(defaultValueNode);
651
+ continue;
652
+ }
601
653
  const constraintNode = parseConstraintValue(tagName, text, provenance);
602
654
  if (constraintNode) {
603
655
  constraints.push(constraintNode);
@@ -605,6 +657,28 @@ function parseTSDocTags(node, file = "") {
605
657
  }
606
658
  return { constraints, annotations };
607
659
  }
660
+ function extractDisplayNameMetadata(node) {
661
+ let displayName;
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;
666
+ const commentText = getTagCommentText(tag);
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);
673
+ continue;
674
+ }
675
+ displayName ??= text;
676
+ }
677
+ return {
678
+ ...displayName !== void 0 && { displayName },
679
+ memberDisplayNames
680
+ };
681
+ }
608
682
  function extractPathTarget(text) {
609
683
  const trimmed = text.trimStart();
610
684
  const match = /^:([a-zA-Z_]\w*)\s+([\s\S]*)$/.exec(trimmed);
@@ -667,7 +741,45 @@ function parseConstraintValue(tagName, text, provenance) {
667
741
  }
668
742
  return null;
669
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
+ }
670
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
+ }
671
783
  const parsed = tryParseJson(effectiveText);
672
784
  if (!Array.isArray(parsed)) {
673
785
  return null;
@@ -699,6 +811,34 @@ function parseConstraintValue(tagName, text, provenance) {
699
811
  provenance
700
812
  };
701
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
+ }
702
842
  function provenanceForComment(range, sourceFile, file, tagName) {
703
843
  const { line, character } = sourceFile.getLineAndCharacterOfPosition(range.pos);
704
844
  return {
@@ -780,11 +920,17 @@ function isObjectType(type) {
780
920
  function isTypeReference(type) {
781
921
  return !!(type.flags & ts4.TypeFlags.Object) && !!(type.objectFlags & ts4.ObjectFlags.Reference);
782
922
  }
923
+ var RESOLVING_TYPE_PLACEHOLDER = {
924
+ kind: "object",
925
+ properties: [],
926
+ additionalProperties: true
927
+ };
783
928
  function analyzeClassToIR(classDecl, checker, file = "") {
784
929
  const name = classDecl.name?.text ?? "AnonymousClass";
785
930
  const fields = [];
786
931
  const fieldLayouts = [];
787
932
  const typeRegistry = {};
933
+ const annotations = extractJSDocAnnotationNodes(classDecl, file);
788
934
  const visiting = /* @__PURE__ */ new Set();
789
935
  const instanceMethods = [];
790
936
  const staticMethods = [];
@@ -807,12 +953,21 @@ function analyzeClassToIR(classDecl, checker, file = "") {
807
953
  }
808
954
  }
809
955
  }
810
- return { name, fields, fieldLayouts, typeRegistry, instanceMethods, staticMethods };
956
+ return {
957
+ name,
958
+ fields,
959
+ fieldLayouts,
960
+ typeRegistry,
961
+ ...annotations.length > 0 && { annotations },
962
+ instanceMethods,
963
+ staticMethods
964
+ };
811
965
  }
812
966
  function analyzeInterfaceToIR(interfaceDecl, checker, file = "") {
813
967
  const name = interfaceDecl.name.text;
814
968
  const fields = [];
815
969
  const typeRegistry = {};
970
+ const annotations = extractJSDocAnnotationNodes(interfaceDecl, file);
816
971
  const visiting = /* @__PURE__ */ new Set();
817
972
  for (const member of interfaceDecl.members) {
818
973
  if (ts4.isPropertySignature(member)) {
@@ -823,7 +978,15 @@ function analyzeInterfaceToIR(interfaceDecl, checker, file = "") {
823
978
  }
824
979
  }
825
980
  const fieldLayouts = fields.map(() => ({}));
826
- return { name, fields, fieldLayouts, typeRegistry, instanceMethods: [], staticMethods: [] };
981
+ return {
982
+ name,
983
+ fields,
984
+ fieldLayouts,
985
+ typeRegistry,
986
+ ...annotations.length > 0 && { annotations },
987
+ instanceMethods: [],
988
+ staticMethods: []
989
+ };
827
990
  }
828
991
  function analyzeTypeAliasToIR(typeAlias, checker, file = "") {
829
992
  if (!ts4.isTypeLiteralNode(typeAlias.type)) {
@@ -838,6 +1001,7 @@ function analyzeTypeAliasToIR(typeAlias, checker, file = "") {
838
1001
  const name = typeAlias.name.text;
839
1002
  const fields = [];
840
1003
  const typeRegistry = {};
1004
+ const annotations = extractJSDocAnnotationNodes(typeAlias, file);
841
1005
  const visiting = /* @__PURE__ */ new Set();
842
1006
  for (const member of typeAlias.type.members) {
843
1007
  if (ts4.isPropertySignature(member)) {
@@ -854,6 +1018,7 @@ function analyzeTypeAliasToIR(typeAlias, checker, file = "") {
854
1018
  fields,
855
1019
  fieldLayouts: fields.map(() => ({})),
856
1020
  typeRegistry,
1021
+ ...annotations.length > 0 && { annotations },
857
1022
  instanceMethods: [],
858
1023
  staticMethods: []
859
1024
  }
@@ -867,7 +1032,7 @@ function analyzeFieldToIR(prop, checker, file, typeRegistry, visiting) {
867
1032
  const tsType = checker.getTypeAtLocation(prop);
868
1033
  const optional = prop.questionToken !== void 0;
869
1034
  const provenance = provenanceForNode(prop, file);
870
- let type = resolveTypeNode(tsType, checker, file, typeRegistry, visiting);
1035
+ let type = resolveTypeNode(tsType, checker, file, typeRegistry, visiting, prop);
871
1036
  const constraints = [];
872
1037
  if (prop.type) {
873
1038
  constraints.push(...extractTypeAliasConstraintNodes(prop.type, checker, file));
@@ -876,7 +1041,7 @@ function analyzeFieldToIR(prop, checker, file, typeRegistry, visiting) {
876
1041
  let annotations = [];
877
1042
  annotations.push(...extractJSDocAnnotationNodes(prop, file));
878
1043
  const defaultAnnotation = extractDefaultValueAnnotation(prop.initializer, file);
879
- if (defaultAnnotation) {
1044
+ if (defaultAnnotation && !annotations.some((a) => a.annotationKind === "defaultValue")) {
880
1045
  annotations.push(defaultAnnotation);
881
1046
  }
882
1047
  ({ type, annotations } = applyEnumMemberDisplayNames(type, annotations));
@@ -898,7 +1063,7 @@ function analyzeInterfacePropertyToIR(prop, checker, file, typeRegistry, visitin
898
1063
  const tsType = checker.getTypeAtLocation(prop);
899
1064
  const optional = prop.questionToken !== void 0;
900
1065
  const provenance = provenanceForNode(prop, file);
901
- let type = resolveTypeNode(tsType, checker, file, typeRegistry, visiting);
1066
+ let type = resolveTypeNode(tsType, checker, file, typeRegistry, visiting, prop);
902
1067
  const constraints = [];
903
1068
  if (prop.type) {
904
1069
  constraints.push(...extractTypeAliasConstraintNodes(prop.type, checker, file));
@@ -979,7 +1144,7 @@ function parseEnumMemberDisplayName(value) {
979
1144
  if (label === "") return null;
980
1145
  return { value: match[1], label };
981
1146
  }
982
- function resolveTypeNode(type, checker, file, typeRegistry, visiting) {
1147
+ function resolveTypeNode(type, checker, file, typeRegistry, visiting, sourceNode) {
983
1148
  if (type.flags & ts4.TypeFlags.String) {
984
1149
  return { kind: "primitive", primitiveKind: "string" };
985
1150
  }
@@ -1008,7 +1173,7 @@ function resolveTypeNode(type, checker, file, typeRegistry, visiting) {
1008
1173
  };
1009
1174
  }
1010
1175
  if (type.isUnion()) {
1011
- return resolveUnionType(type, checker, file, typeRegistry, visiting);
1176
+ return resolveUnionType(type, checker, file, typeRegistry, visiting, sourceNode);
1012
1177
  }
1013
1178
  if (checker.isArrayType(type)) {
1014
1179
  return resolveArrayType(type, checker, file, typeRegistry, visiting);
@@ -1018,70 +1183,102 @@ function resolveTypeNode(type, checker, file, typeRegistry, visiting) {
1018
1183
  }
1019
1184
  return { kind: "primitive", primitiveKind: "string" };
1020
1185
  }
1021
- 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
+ }
1022
1192
  const allTypes = type.types;
1023
1193
  const nonNullTypes = allTypes.filter(
1024
1194
  (t) => !(t.flags & (ts4.TypeFlags.Null | ts4.TypeFlags.Undefined))
1025
1195
  );
1026
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
+ });
1027
1225
  const isBooleanUnion2 = nonNullTypes.length === 2 && nonNullTypes.every((t) => t.flags & ts4.TypeFlags.BooleanLiteral);
1028
1226
  if (isBooleanUnion2) {
1029
1227
  const boolNode = { kind: "primitive", primitiveKind: "boolean" };
1030
- if (hasNull) {
1031
- return {
1032
- kind: "union",
1033
- members: [boolNode, { kind: "primitive", primitiveKind: "null" }]
1034
- };
1035
- }
1036
- return boolNode;
1228
+ const result = hasNull ? {
1229
+ kind: "union",
1230
+ members: [boolNode, { kind: "primitive", primitiveKind: "null" }]
1231
+ } : boolNode;
1232
+ return registerNamed(result);
1037
1233
  }
1038
1234
  const allStringLiterals = nonNullTypes.every((t) => t.isStringLiteral());
1039
1235
  if (allStringLiterals && nonNullTypes.length > 0) {
1040
1236
  const stringTypes = nonNullTypes.filter((t) => t.isStringLiteral());
1041
1237
  const enumNode = {
1042
1238
  kind: "enum",
1043
- members: stringTypes.map((t) => ({ value: t.value }))
1239
+ members: applyMemberLabels(stringTypes.map((t) => t.value))
1044
1240
  };
1045
- if (hasNull) {
1046
- return {
1047
- kind: "union",
1048
- members: [enumNode, { kind: "primitive", primitiveKind: "null" }]
1049
- };
1050
- }
1051
- return enumNode;
1241
+ const result = hasNull ? {
1242
+ kind: "union",
1243
+ members: [enumNode, { kind: "primitive", primitiveKind: "null" }]
1244
+ } : enumNode;
1245
+ return registerNamed(result);
1052
1246
  }
1053
1247
  const allNumberLiterals = nonNullTypes.every((t) => t.isNumberLiteral());
1054
1248
  if (allNumberLiterals && nonNullTypes.length > 0) {
1055
1249
  const numberTypes = nonNullTypes.filter((t) => t.isNumberLiteral());
1056
1250
  const enumNode = {
1057
1251
  kind: "enum",
1058
- members: numberTypes.map((t) => ({ value: t.value }))
1252
+ members: applyMemberLabels(numberTypes.map((t) => t.value))
1059
1253
  };
1060
- if (hasNull) {
1061
- return {
1062
- kind: "union",
1063
- members: [enumNode, { kind: "primitive", primitiveKind: "null" }]
1064
- };
1065
- }
1066
- return enumNode;
1254
+ const result = hasNull ? {
1255
+ kind: "union",
1256
+ members: [enumNode, { kind: "primitive", primitiveKind: "null" }]
1257
+ } : enumNode;
1258
+ return registerNamed(result);
1067
1259
  }
1068
1260
  if (nonNullTypes.length === 1 && nonNullTypes[0]) {
1069
- const inner = resolveTypeNode(nonNullTypes[0], checker, file, typeRegistry, visiting);
1070
- if (hasNull) {
1071
- return {
1072
- kind: "union",
1073
- members: [inner, { kind: "primitive", primitiveKind: "null" }]
1074
- };
1075
- }
1076
- return inner;
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);
1077
1274
  }
1078
1275
  const members = nonNullTypes.map(
1079
- (t) => resolveTypeNode(t, checker, file, typeRegistry, visiting)
1276
+ (t) => resolveTypeNode(t, checker, file, typeRegistry, visiting, sourceNode)
1080
1277
  );
1081
1278
  if (hasNull) {
1082
1279
  members.push({ kind: "primitive", primitiveKind: "null" });
1083
1280
  }
1084
- return { kind: "union", members };
1281
+ return registerNamed({ kind: "union", members });
1085
1282
  }
1086
1283
  function resolveArrayType(type, checker, file, typeRegistry, visiting) {
1087
1284
  const typeArgs = isTypeReference(type) ? type.typeArguments : void 0;
@@ -1097,30 +1294,84 @@ function tryResolveRecordType(type, checker, file, typeRegistry, visiting) {
1097
1294
  if (!indexInfo) {
1098
1295
  return null;
1099
1296
  }
1100
- if (visiting.has(type)) {
1101
- return null;
1102
- }
1103
- visiting.add(type);
1104
- try {
1105
- const valueType = resolveTypeNode(indexInfo.type, checker, file, typeRegistry, visiting);
1106
- return { kind: "record", valueType };
1107
- } finally {
1108
- visiting.delete(type);
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
+ }
1109
1323
  }
1110
1324
  }
1111
1325
  function resolveObjectType(type, checker, file, typeRegistry, visiting) {
1112
- const recordNode = tryResolveRecordType(type, checker, file, typeRegistry, visiting);
1113
- if (recordNode) {
1114
- return recordNode;
1115
- }
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
+ };
1116
1336
  if (visiting.has(type)) {
1337
+ if (namedTypeName !== void 0 && shouldRegisterNamedType) {
1338
+ return { kind: "reference", name: namedTypeName, typeArguments: [] };
1339
+ }
1117
1340
  return { kind: "object", properties: [], additionalProperties: false };
1118
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
+ }
1119
1349
  visiting.add(type);
1120
- const typeName = getNamedTypeName(type);
1121
- if (typeName && typeName in typeRegistry) {
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) {
1122
1358
  visiting.delete(type);
1123
- return { kind: "reference", name: typeName, typeArguments: [] };
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;
1124
1375
  }
1125
1376
  const properties = [];
1126
1377
  const fieldInfoMap = getNamedTypeFieldNodeInfoMap(type, checker, file, typeRegistry, visiting);
@@ -1129,7 +1380,14 @@ function resolveObjectType(type, checker, file, typeRegistry, visiting) {
1129
1380
  if (!declaration) continue;
1130
1381
  const propType = checker.getTypeOfSymbolAtLocation(prop, declaration);
1131
1382
  const optional = !!(prop.flags & ts4.SymbolFlags.Optional);
1132
- const propTypeNode = resolveTypeNode(propType, checker, file, typeRegistry, visiting);
1383
+ const propTypeNode = resolveTypeNode(
1384
+ propType,
1385
+ checker,
1386
+ file,
1387
+ typeRegistry,
1388
+ visiting,
1389
+ declaration
1390
+ );
1133
1391
  const fieldNodeInfo = fieldInfoMap?.get(prop.name);
1134
1392
  properties.push({
1135
1393
  name: prop.name,
@@ -1146,13 +1404,15 @@ function resolveObjectType(type, checker, file, typeRegistry, visiting) {
1146
1404
  properties,
1147
1405
  additionalProperties: true
1148
1406
  };
1149
- if (typeName) {
1150
- typeRegistry[typeName] = {
1151
- name: typeName,
1407
+ if (namedTypeName !== void 0 && shouldRegisterNamedType) {
1408
+ const annotations = namedDecl ? extractJSDocAnnotationNodes(namedDecl, file) : void 0;
1409
+ typeRegistry[namedTypeName] = {
1410
+ name: namedTypeName,
1152
1411
  type: objectNode,
1153
- provenance: provenanceForFile(file)
1412
+ ...annotations !== void 0 && annotations.length > 0 && { annotations },
1413
+ provenance: provenanceForDeclaration(namedDecl, file)
1154
1414
  };
1155
- return { kind: "reference", name: typeName, typeArguments: [] };
1415
+ return { kind: "reference", name: namedTypeName, typeArguments: [] };
1156
1416
  }
1157
1417
  return objectNode;
1158
1418
  }
@@ -1244,6 +1504,12 @@ function provenanceForNode(node, file) {
1244
1504
  function provenanceForFile(file) {
1245
1505
  return { surface: "tsdoc", file, line: 0, column: 0 };
1246
1506
  }
1507
+ function provenanceForDeclaration(node, file) {
1508
+ if (!node) {
1509
+ return provenanceForFile(file);
1510
+ }
1511
+ return provenanceForNode(node, file);
1512
+ }
1247
1513
  function getNamedTypeName(type) {
1248
1514
  const symbol = type.getSymbol();
1249
1515
  if (symbol?.declarations) {
@@ -1262,6 +1528,20 @@ function getNamedTypeName(type) {
1262
1528
  }
1263
1529
  return null;
1264
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
+ }
1265
1545
  function analyzeMethod(method, checker) {
1266
1546
  if (!ts4.isIdentifier(method.name)) {
1267
1547
  return null;
@@ -1321,6 +1601,9 @@ function generateJsonSchemaFromIR(ir, options) {
1321
1601
  const ctx = makeContext(options);
1322
1602
  for (const [name, typeDef] of Object.entries(ir.typeRegistry)) {
1323
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
+ }
1324
1607
  }
1325
1608
  const properties = {};
1326
1609
  const required = [];
@@ -1332,6 +1615,9 @@ function generateJsonSchemaFromIR(ir, options) {
1332
1615
  properties,
1333
1616
  ...uniqueRequired.length > 0 && { required: uniqueRequired }
1334
1617
  };
1618
+ if (ir.annotations && ir.annotations.length > 0) {
1619
+ applyAnnotations(result, ir.annotations, ctx);
1620
+ }
1335
1621
  if (Object.keys(ctx.defs).length > 0) {
1336
1622
  result.$defs = ctx.defs;
1337
1623
  }
@@ -1361,22 +1647,51 @@ function collectFields(elements, properties, required, ctx) {
1361
1647
  }
1362
1648
  function generateFieldSchema(field, ctx) {
1363
1649
  const schema = generateTypeNode(field.type, ctx);
1650
+ const itemStringSchema = schema.type === "array" && schema.items?.type === "string" ? schema.items : void 0;
1364
1651
  const directConstraints = [];
1652
+ const itemConstraints = [];
1365
1653
  const pathConstraints = [];
1366
1654
  for (const c of field.constraints) {
1367
1655
  if (c.path) {
1368
1656
  pathConstraints.push(c);
1657
+ } else if (itemStringSchema !== void 0 && isStringItemConstraint(c)) {
1658
+ itemConstraints.push(c);
1369
1659
  } else {
1370
1660
  directConstraints.push(c);
1371
1661
  }
1372
1662
  }
1373
1663
  applyConstraints(schema, directConstraints, ctx);
1374
- applyAnnotations(schema, field.annotations, 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
+ }
1375
1680
  if (pathConstraints.length === 0) {
1376
1681
  return schema;
1377
1682
  }
1378
1683
  return applyPathTargetedConstraints(schema, pathConstraints, ctx);
1379
1684
  }
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
+ }
1380
1695
  function applyPathTargetedConstraints(schema, pathConstraints, ctx) {
1381
1696
  if (schema.type === "array" && schema.items) {
1382
1697
  schema.items = applyPathTargetedConstraints(schema.items, pathConstraints, ctx);
@@ -1594,6 +1909,9 @@ function applyConstraints(schema, constraints, ctx) {
1594
1909
  case "uniqueItems":
1595
1910
  schema.uniqueItems = constraint.value;
1596
1911
  break;
1912
+ case "const":
1913
+ schema.const = constraint.value;
1914
+ break;
1597
1915
  case "allowedMembers":
1598
1916
  break;
1599
1917
  case "custom":
@@ -1618,8 +1936,14 @@ function applyAnnotations(schema, annotations, ctx) {
1618
1936
  case "defaultValue":
1619
1937
  schema.default = annotation.value;
1620
1938
  break;
1939
+ case "format":
1940
+ schema.format = annotation.value;
1941
+ break;
1621
1942
  case "deprecated":
1622
1943
  schema.deprecated = true;
1944
+ if (annotation.message !== void 0 && annotation.message !== "") {
1945
+ schema["x-formspec-deprecation-description"] = annotation.message;
1946
+ }
1623
1947
  break;
1624
1948
  case "placeholder":
1625
1949
  break;
@@ -1801,25 +2125,31 @@ function createShowRule(fieldName, value) {
1801
2125
  }
1802
2126
  };
1803
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
+ }
1804
2144
  function combineRules(parentRule, childRule) {
1805
- const parentCondition = parentRule.condition;
1806
- const childCondition = childRule.condition;
1807
2145
  return {
1808
2146
  effect: "SHOW",
1809
2147
  condition: {
1810
2148
  scope: "#",
1811
2149
  schema: {
1812
2150
  allOf: [
1813
- {
1814
- properties: {
1815
- [parentCondition.scope.replace("#/properties/", "")]: parentCondition.schema
1816
- }
1817
- },
1818
- {
1819
- properties: {
1820
- [childCondition.scope.replace("#/properties/", "")]: childCondition.schema
1821
- }
1822
- }
2151
+ ...flattenConditionSchema(parentRule.condition.scope, parentRule.condition.schema),
2152
+ ...flattenConditionSchema(childRule.condition.scope, childRule.condition.schema)
1823
2153
  ]
1824
2154
  }
1825
2155
  }
@@ -1827,10 +2157,14 @@ function combineRules(parentRule, childRule) {
1827
2157
  }
1828
2158
  function fieldNodeToControl(field, parentRule) {
1829
2159
  const displayNameAnnotation = field.annotations.find((a) => a.annotationKind === "displayName");
2160
+ const placeholderAnnotation = field.annotations.find((a) => a.annotationKind === "placeholder");
1830
2161
  const control = {
1831
2162
  type: "Control",
1832
2163
  scope: fieldToScope(field.name),
1833
2164
  ...displayNameAnnotation !== void 0 && { label: displayNameAnnotation.value },
2165
+ ...placeholderAnnotation !== void 0 && {
2166
+ options: { placeholder: placeholderAnnotation.value }
2167
+ },
1834
2168
  ...parentRule !== void 0 && { rule: parentRule }
1835
2169
  };
1836
2170
  return control;
@@ -1916,6 +2250,15 @@ function addUnknownExtension(ctx, message, primary) {
1916
2250
  relatedLocations: []
1917
2251
  });
1918
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
+ }
1919
2262
  function addConstraintBroadening(ctx, message, primary, related) {
1920
2263
  ctx.diagnostics.push({
1921
2264
  code: "CONSTRAINT_BROADENING",
@@ -1936,6 +2279,45 @@ function findAllowedMembers(constraints) {
1936
2279
  (c) => c.constraintKind === "allowedMembers"
1937
2280
  );
1938
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
+ }
1939
2321
  function isOrderedBoundConstraint(constraint) {
1940
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";
1941
2323
  }
@@ -2142,6 +2524,25 @@ function checkAllowedMembersContradiction(ctx, fieldName, constraints) {
2142
2524
  }
2143
2525
  }
2144
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
+ }
2145
2546
  function typeLabel(type) {
2146
2547
  switch (type.kind) {
2147
2548
  case "primitive":
@@ -2214,6 +2615,8 @@ function checkConstraintOnType(ctx, fieldName, type, constraint) {
2214
2615
  const isString = effectiveType.kind === "primitive" && effectiveType.primitiveKind === "string";
2215
2616
  const isArray = effectiveType.kind === "array";
2216
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";
2217
2620
  const label = typeLabel(effectiveType);
2218
2621
  const ck = constraint.constraintKind;
2219
2622
  switch (ck) {
@@ -2234,10 +2637,10 @@ function checkConstraintOnType(ctx, fieldName, type, constraint) {
2234
2637
  case "minLength":
2235
2638
  case "maxLength":
2236
2639
  case "pattern": {
2237
- if (!isString) {
2640
+ if (!isString && !isStringArray) {
2238
2641
  addTypeMismatch(
2239
2642
  ctx,
2240
- `Field "${fieldName}": constraint "${ck}" is only valid on string fields, but field type is "${label}"`,
2643
+ `Field "${fieldName}": constraint "${ck}" is only valid on string fields or string array items, but field type is "${label}"`,
2241
2644
  constraint.provenance
2242
2645
  );
2243
2646
  }
@@ -2265,6 +2668,37 @@ function checkConstraintOnType(ctx, fieldName, type, constraint) {
2265
2668
  }
2266
2669
  break;
2267
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
+ );
2679
+ break;
2680
+ }
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) {
2684
+ addTypeMismatch(
2685
+ ctx,
2686
+ `Field "${fieldName}": @const value type "${valueType}" is incompatible with field type "${effectiveType.primitiveKind}"`,
2687
+ constraint.provenance
2688
+ );
2689
+ }
2690
+ break;
2691
+ }
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
+ );
2699
+ }
2700
+ break;
2701
+ }
2268
2702
  case "custom": {
2269
2703
  checkCustomConstraint(ctx, fieldName, effectiveType, constraint);
2270
2704
  break;
@@ -2283,9 +2717,9 @@ function checkTypeApplicability(ctx, fieldName, type, constraints) {
2283
2717
  const resolution = resolvePathTargetType(ctx, type, constraint.path.segments);
2284
2718
  const targetFieldName = formatPathTargetFieldName(fieldName, constraint.path.segments);
2285
2719
  if (resolution.kind === "missing-property") {
2286
- addTypeMismatch(
2720
+ addUnknownPathTarget(
2287
2721
  ctx,
2288
- `Field "${fieldName}": path-targeted constraint "${constraint.constraintKind}" references unknown path segment "${resolution.segment}"`,
2722
+ `Field "${targetFieldName}": path-targeted constraint "${constraint.constraintKind}" references unknown path segment "${resolution.segment}"`,
2289
2723
  constraint.provenance
2290
2724
  );
2291
2725
  continue;
@@ -2345,6 +2779,7 @@ function validateConstraints(ctx, name, type, constraints) {
2345
2779
  checkNumericContradictions(ctx, name, constraints);
2346
2780
  checkLengthContradictions(ctx, name, constraints);
2347
2781
  checkAllowedMembersContradiction(ctx, name, constraints);
2782
+ checkConstContradictions(ctx, name, constraints);
2348
2783
  checkConstraintBroadening(ctx, name, constraints);
2349
2784
  checkTypeApplicability(ctx, name, type, constraints);
2350
2785
  }