@formspec/build 0.1.0-alpha.19 → 0.1.0-alpha.20

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 (35) hide show
  1. package/dist/__tests__/fixtures/class-schema-regressions.d.ts +4 -0
  2. package/dist/__tests__/fixtures/class-schema-regressions.d.ts.map +1 -1
  3. package/dist/__tests__/parity/utils.d.ts.map +1 -1
  4. package/dist/analyzer/class-analyzer.d.ts +4 -1
  5. package/dist/analyzer/class-analyzer.d.ts.map +1 -1
  6. package/dist/analyzer/jsdoc-constraints.d.ts +2 -1
  7. package/dist/analyzer/jsdoc-constraints.d.ts.map +1 -1
  8. package/dist/analyzer/tsdoc-parser.d.ts +13 -3
  9. package/dist/analyzer/tsdoc-parser.d.ts.map +1 -1
  10. package/dist/browser.cjs +30 -748
  11. package/dist/browser.cjs.map +1 -1
  12. package/dist/browser.js +32 -748
  13. package/dist/browser.js.map +1 -1
  14. package/dist/build.d.ts +14 -14
  15. package/dist/cli.cjs +691 -1101
  16. package/dist/cli.cjs.map +1 -1
  17. package/dist/cli.js +704 -1100
  18. package/dist/cli.js.map +1 -1
  19. package/dist/generators/class-schema.d.ts.map +1 -1
  20. package/dist/index.cjs +689 -1095
  21. package/dist/index.cjs.map +1 -1
  22. package/dist/index.js +703 -1095
  23. package/dist/index.js.map +1 -1
  24. package/dist/internals.cjs +663 -1069
  25. package/dist/internals.cjs.map +1 -1
  26. package/dist/internals.js +675 -1067
  27. package/dist/internals.js.map +1 -1
  28. package/dist/ui-schema/schema.d.ts +14 -14
  29. package/dist/validate/constraint-validator.d.ts +6 -45
  30. package/dist/validate/constraint-validator.d.ts.map +1 -1
  31. package/package.json +2 -1
  32. package/dist/__tests__/json-utils.test.d.ts +0 -5
  33. package/dist/__tests__/json-utils.test.d.ts.map +0 -1
  34. package/dist/analyzer/json-utils.d.ts +0 -22
  35. package/dist/analyzer/json-utils.d.ts.map +0 -1
@@ -449,32 +449,9 @@ var ts2 = __toESM(require("typescript"), 1);
449
449
 
450
450
  // src/analyzer/tsdoc-parser.ts
451
451
  var ts = __toESM(require("typescript"), 1);
452
+ var import_analysis = require("@formspec/analysis");
452
453
  var import_tsdoc = require("@microsoft/tsdoc");
453
454
  var import_core3 = require("@formspec/core");
454
-
455
- // src/analyzer/json-utils.ts
456
- function tryParseJson(text) {
457
- try {
458
- return JSON.parse(text);
459
- } catch {
460
- return null;
461
- }
462
- }
463
-
464
- // src/analyzer/tsdoc-parser.ts
465
- var NUMERIC_CONSTRAINT_MAP = {
466
- minimum: "minimum",
467
- maximum: "maximum",
468
- exclusiveMinimum: "exclusiveMinimum",
469
- exclusiveMaximum: "exclusiveMaximum",
470
- multipleOf: "multipleOf"
471
- };
472
- var LENGTH_CONSTRAINT_MAP = {
473
- minLength: "minLength",
474
- maxLength: "maxLength",
475
- minItems: "minItems",
476
- maxItems: "maxItems"
477
- };
478
455
  var TAGS_REQUIRING_RAW_TEXT = /* @__PURE__ */ new Set(["pattern", "enumOptions", "defaultValue"]);
479
456
  function createFormSpecTSDocConfig(extensionTagNames = []) {
480
457
  const config = new import_tsdoc.TSDocConfiguration();
@@ -507,7 +484,294 @@ function createFormSpecTSDocConfig(extensionTagNames = []) {
507
484
  }
508
485
  return config;
509
486
  }
487
+ function sharedCommentSyntaxOptions(options, offset) {
488
+ const extensions = options?.extensionRegistry?.extensions;
489
+ return {
490
+ ...offset !== void 0 ? { offset } : {},
491
+ ...extensions !== void 0 ? { extensions } : {}
492
+ };
493
+ }
494
+ function sharedTagValueOptions(options) {
495
+ return {
496
+ ...options?.extensionRegistry !== void 0 ? { registry: options.extensionRegistry } : {},
497
+ ...options?.fieldType !== void 0 ? { fieldType: options.fieldType } : {}
498
+ };
499
+ }
500
+ var SYNTHETIC_TYPE_FORMAT_FLAGS = ts.TypeFormatFlags.NoTruncation | ts.TypeFormatFlags.UseAliasDefinedOutsideCurrentScope;
501
+ function buildSupportingDeclarations(sourceFile) {
502
+ return sourceFile.statements.filter(
503
+ (statement) => !ts.isImportDeclaration(statement) && !ts.isImportEqualsDeclaration(statement) && !(ts.isExportDeclaration(statement) && statement.moduleSpecifier !== void 0)
504
+ ).map((statement) => statement.getText(sourceFile));
505
+ }
506
+ function renderSyntheticArgumentExpression(valueKind, argumentText) {
507
+ const trimmed = argumentText.trim();
508
+ if (trimmed === "") {
509
+ return null;
510
+ }
511
+ switch (valueKind) {
512
+ case "number":
513
+ case "integer":
514
+ case "signedInteger":
515
+ return Number.isFinite(Number(trimmed)) ? trimmed : JSON.stringify(trimmed);
516
+ case "string":
517
+ return JSON.stringify(argumentText);
518
+ case "json":
519
+ try {
520
+ JSON.parse(trimmed);
521
+ return `(${trimmed})`;
522
+ } catch {
523
+ return JSON.stringify(trimmed);
524
+ }
525
+ case "boolean":
526
+ return trimmed === "true" || trimmed === "false" ? trimmed : JSON.stringify(trimmed);
527
+ case "condition":
528
+ return "undefined as unknown as FormSpecCondition";
529
+ case null:
530
+ return null;
531
+ default: {
532
+ return String(valueKind);
533
+ }
534
+ }
535
+ }
536
+ function getArrayElementType(type, checker) {
537
+ if (!checker.isArrayType(type)) {
538
+ return null;
539
+ }
540
+ return checker.getTypeArguments(type)[0] ?? null;
541
+ }
542
+ function supportsConstraintCapability(type, checker, capability) {
543
+ if (capability === void 0) {
544
+ return true;
545
+ }
546
+ if ((0, import_analysis.hasTypeSemanticCapability)(type, checker, capability)) {
547
+ return true;
548
+ }
549
+ if (capability === "string-like") {
550
+ const itemType = getArrayElementType(type, checker);
551
+ return itemType !== null && (0, import_analysis.hasTypeSemanticCapability)(itemType, checker, capability);
552
+ }
553
+ return false;
554
+ }
555
+ function makeDiagnostic(code, message, provenance) {
556
+ return {
557
+ code,
558
+ message,
559
+ severity: "error",
560
+ primaryLocation: provenance,
561
+ relatedLocations: []
562
+ };
563
+ }
564
+ function placementLabel(placement) {
565
+ switch (placement) {
566
+ case "class":
567
+ return "class declarations";
568
+ case "class-field":
569
+ return "class fields";
570
+ case "class-method":
571
+ return "class methods";
572
+ case "interface":
573
+ return "interface declarations";
574
+ case "interface-field":
575
+ return "interface fields";
576
+ case "type-alias":
577
+ return "type aliases";
578
+ case "type-alias-field":
579
+ return "type-alias properties";
580
+ case "variable":
581
+ return "variables";
582
+ case "function":
583
+ return "functions";
584
+ case "function-parameter":
585
+ return "function parameters";
586
+ case "method-parameter":
587
+ return "method parameters";
588
+ default: {
589
+ const exhaustive = placement;
590
+ return String(exhaustive);
591
+ }
592
+ }
593
+ }
594
+ function capabilityLabel(capability) {
595
+ switch (capability) {
596
+ case "numeric-comparable":
597
+ return "number";
598
+ case "string-like":
599
+ return "string";
600
+ case "array-like":
601
+ return "array";
602
+ case "enum-member-addressable":
603
+ return "enum";
604
+ case "json-like":
605
+ return "JSON-compatible";
606
+ case "object-like":
607
+ return "object";
608
+ case "condition-like":
609
+ return "conditional";
610
+ case void 0:
611
+ return "compatible";
612
+ default:
613
+ return capability;
614
+ }
615
+ }
616
+ function getBroadenedCustomTypeId(fieldType) {
617
+ if (fieldType?.kind === "custom") {
618
+ return fieldType.typeId;
619
+ }
620
+ if (fieldType?.kind !== "union") {
621
+ return void 0;
622
+ }
623
+ const customMembers = fieldType.members.filter(
624
+ (member) => member.kind === "custom"
625
+ );
626
+ if (customMembers.length !== 1) {
627
+ return void 0;
628
+ }
629
+ const nonCustomMembers = fieldType.members.filter((member) => member.kind !== "custom");
630
+ const allOtherMembersAreNull = nonCustomMembers.every(
631
+ (member) => member.kind === "primitive" && member.primitiveKind === "null"
632
+ );
633
+ const customMember = customMembers[0];
634
+ return allOtherMembersAreNull && customMember !== void 0 ? customMember.typeId : void 0;
635
+ }
636
+ function hasBuiltinConstraintBroadening(tagName, options) {
637
+ const broadenedTypeId = getBroadenedCustomTypeId(options?.fieldType);
638
+ return broadenedTypeId !== void 0 && options?.extensionRegistry?.findBuiltinConstraintBroadening(broadenedTypeId, tagName) !== void 0;
639
+ }
640
+ function buildCompilerBackedConstraintDiagnostics(node, sourceFile, tagName, parsedTag, provenance, supportingDeclarations, options) {
641
+ if (!(0, import_core3.isBuiltinConstraintName)(tagName)) {
642
+ return [];
643
+ }
644
+ const checker = options?.checker;
645
+ const subjectType = options?.subjectType;
646
+ if (checker === void 0 || subjectType === void 0) {
647
+ return [];
648
+ }
649
+ const placement = (0, import_analysis.resolveDeclarationPlacement)(node);
650
+ if (placement === null) {
651
+ return [];
652
+ }
653
+ const definition = (0, import_analysis.getTagDefinition)(tagName, options?.extensionRegistry?.extensions);
654
+ if (definition === null) {
655
+ return [];
656
+ }
657
+ if (!definition.placements.includes(placement)) {
658
+ return [
659
+ makeDiagnostic(
660
+ "INVALID_TAG_PLACEMENT",
661
+ `Tag "@${tagName}" is not allowed on ${placementLabel(placement)}.`,
662
+ provenance
663
+ )
664
+ ];
665
+ }
666
+ const target = parsedTag?.target ?? null;
667
+ const hasBroadening = target === null && hasBuiltinConstraintBroadening(tagName, options);
668
+ if (target !== null) {
669
+ if (target.kind !== "path") {
670
+ return [
671
+ makeDiagnostic(
672
+ "UNSUPPORTED_TARGETING_SYNTAX",
673
+ `Tag "@${tagName}" does not support ${target.kind} targeting syntax.`,
674
+ provenance
675
+ )
676
+ ];
677
+ }
678
+ if (!target.valid || target.path === null) {
679
+ return [
680
+ makeDiagnostic(
681
+ "UNSUPPORTED_TARGETING_SYNTAX",
682
+ `Tag "@${tagName}" has invalid path targeting syntax.`,
683
+ provenance
684
+ )
685
+ ];
686
+ }
687
+ const resolution = (0, import_analysis.resolvePathTargetType)(subjectType, checker, target.path.segments);
688
+ if (resolution.kind === "missing-property") {
689
+ return [
690
+ makeDiagnostic(
691
+ "UNKNOWN_PATH_TARGET",
692
+ `Target "${target.rawText}": path-targeted constraint "${tagName}" references unknown path segment "${resolution.segment}"`,
693
+ provenance
694
+ )
695
+ ];
696
+ }
697
+ if (resolution.kind === "unresolvable") {
698
+ const actualType = checker.typeToString(resolution.type, node, SYNTHETIC_TYPE_FORMAT_FLAGS);
699
+ return [
700
+ makeDiagnostic(
701
+ "TYPE_MISMATCH",
702
+ `Target "${target.rawText}": path-targeted constraint "${tagName}" is invalid because type "${actualType}" cannot be traversed`,
703
+ provenance
704
+ )
705
+ ];
706
+ }
707
+ const requiredCapability = definition.capabilities[0];
708
+ if (requiredCapability !== void 0 && !supportsConstraintCapability(resolution.type, checker, requiredCapability)) {
709
+ const actualType = checker.typeToString(resolution.type, node, SYNTHETIC_TYPE_FORMAT_FLAGS);
710
+ return [
711
+ makeDiagnostic(
712
+ "TYPE_MISMATCH",
713
+ `Target "${target.rawText}": constraint "${tagName}" is only valid on ${capabilityLabel(requiredCapability)} targets, but field type is "${actualType}"`,
714
+ provenance
715
+ )
716
+ ];
717
+ }
718
+ } else if (!hasBroadening) {
719
+ const requiredCapability = definition.capabilities[0];
720
+ if (requiredCapability !== void 0 && !supportsConstraintCapability(subjectType, checker, requiredCapability)) {
721
+ const actualType = checker.typeToString(subjectType, node, SYNTHETIC_TYPE_FORMAT_FLAGS);
722
+ return [
723
+ makeDiagnostic(
724
+ "TYPE_MISMATCH",
725
+ `Target "${node.getText(sourceFile)}": constraint "${tagName}" is only valid on ${capabilityLabel(requiredCapability)} targets, but field type is "${actualType}"`,
726
+ provenance
727
+ )
728
+ ];
729
+ }
730
+ }
731
+ const argumentExpression = renderSyntheticArgumentExpression(
732
+ definition.valueKind,
733
+ parsedTag?.argumentText ?? ""
734
+ );
735
+ if (definition.requiresArgument && argumentExpression === null) {
736
+ return [];
737
+ }
738
+ if (hasBroadening) {
739
+ return [];
740
+ }
741
+ const subjectTypeText = checker.typeToString(subjectType, node, SYNTHETIC_TYPE_FORMAT_FLAGS);
742
+ const hostType = options?.hostType ?? subjectType;
743
+ const hostTypeText = checker.typeToString(hostType, node, SYNTHETIC_TYPE_FORMAT_FLAGS);
744
+ const result = (0, import_analysis.checkSyntheticTagApplication)({
745
+ tagName,
746
+ placement,
747
+ hostType: hostTypeText,
748
+ subjectType: subjectTypeText,
749
+ ...target?.kind === "path" ? { target: { kind: "path", text: target.rawText } } : {},
750
+ ...argumentExpression !== null ? { argumentExpression } : {},
751
+ supportingDeclarations,
752
+ ...options?.extensionRegistry !== void 0 ? {
753
+ extensions: options.extensionRegistry.extensions.map((extension) => ({
754
+ extensionId: extension.extensionId,
755
+ ...extension.constraintTags !== void 0 ? {
756
+ constraintTags: extension.constraintTags.map((tag) => ({ tagName: tag.tagName }))
757
+ } : {}
758
+ }))
759
+ } : {}
760
+ });
761
+ if (result.diagnostics.length === 0) {
762
+ return [];
763
+ }
764
+ const expectedLabel = definition.valueKind === null ? "compatible argument" : capabilityLabel(definition.valueKind);
765
+ return [
766
+ makeDiagnostic(
767
+ "TYPE_MISMATCH",
768
+ `Tag "@${tagName}" received an invalid argument for ${expectedLabel}.`,
769
+ provenance
770
+ )
771
+ ];
772
+ }
510
773
  var parserCache = /* @__PURE__ */ new Map();
774
+ var parseResultCache = /* @__PURE__ */ new Map();
511
775
  function getParser(options) {
512
776
  const extensionTagNames = [
513
777
  ...options?.extensionRegistry?.extensions.flatMap(
@@ -523,18 +787,54 @@ function getParser(options) {
523
787
  parserCache.set(cacheKey, parser);
524
788
  return parser;
525
789
  }
790
+ function getExtensionRegistryCacheKey(registry) {
791
+ if (registry === void 0) {
792
+ return "";
793
+ }
794
+ return registry.extensions.map(
795
+ (extension) => JSON.stringify({
796
+ extensionId: extension.extensionId,
797
+ typeNames: extension.types?.map((type) => type.typeName) ?? [],
798
+ constraintTags: extension.constraintTags?.map((tag) => tag.tagName) ?? []
799
+ })
800
+ ).join("|");
801
+ }
802
+ function getParseCacheKey(node, file, options) {
803
+ const sourceFile = node.getSourceFile();
804
+ const checker = options?.checker;
805
+ return JSON.stringify({
806
+ file,
807
+ sourceFile: sourceFile.fileName,
808
+ sourceText: sourceFile.text,
809
+ start: node.getFullStart(),
810
+ end: node.getEnd(),
811
+ fieldType: options?.fieldType ?? null,
812
+ subjectType: checker !== void 0 && options?.subjectType !== void 0 ? checker.typeToString(options.subjectType, node, SYNTHETIC_TYPE_FORMAT_FLAGS) : null,
813
+ hostType: checker !== void 0 && options?.hostType !== void 0 ? checker.typeToString(options.hostType, node, SYNTHETIC_TYPE_FORMAT_FLAGS) : null,
814
+ extensions: getExtensionRegistryCacheKey(options?.extensionRegistry)
815
+ });
816
+ }
526
817
  function parseTSDocTags(node, file = "", options) {
818
+ const cacheKey = getParseCacheKey(node, file, options);
819
+ const cached = parseResultCache.get(cacheKey);
820
+ if (cached !== void 0) {
821
+ return cached;
822
+ }
527
823
  const constraints = [];
528
824
  const annotations = [];
825
+ const diagnostics = [];
529
826
  let displayName;
530
827
  let description;
531
828
  let placeholder;
532
829
  let displayNameProvenance;
533
830
  let descriptionProvenance;
534
831
  let placeholderProvenance;
832
+ const rawTextTags = [];
535
833
  const sourceFile = node.getSourceFile();
536
834
  const sourceText = sourceFile.getFullText();
835
+ const supportingDeclarations = buildSupportingDeclarations(sourceFile);
537
836
  const commentRanges = ts.getLeadingCommentRanges(sourceText, node.getFullStart());
837
+ const rawTextFallbacks = collectRawTextFallbacks(node, file);
538
838
  if (commentRanges) {
539
839
  for (const range of commentRanges) {
540
840
  if (range.kind !== ts.SyntaxKind.MultiLineCommentTrivia) {
@@ -549,12 +849,33 @@ function parseTSDocTags(node, file = "", options) {
549
849
  import_tsdoc.TextRange.fromStringRange(sourceText, range.pos, range.end)
550
850
  );
551
851
  const docComment = parserContext.docComment;
852
+ const parsedComment = (0, import_analysis.parseCommentBlock)(
853
+ commentText,
854
+ sharedCommentSyntaxOptions(options, range.pos)
855
+ );
856
+ let parsedTagCursor = 0;
857
+ const nextParsedTag = (normalizedTagName) => {
858
+ while (parsedTagCursor < parsedComment.tags.length) {
859
+ const candidate = parsedComment.tags[parsedTagCursor];
860
+ parsedTagCursor += 1;
861
+ if (candidate?.normalizedTagName === normalizedTagName) {
862
+ return candidate;
863
+ }
864
+ }
865
+ return null;
866
+ };
867
+ for (const parsedTag of parsedComment.tags) {
868
+ if (TAGS_REQUIRING_RAW_TEXT.has(parsedTag.normalizedTagName)) {
869
+ rawTextTags.push({ tag: parsedTag, commentText, commentOffset: range.pos });
870
+ }
871
+ }
552
872
  for (const block of docComment.customBlocks) {
553
873
  const tagName = (0, import_core3.normalizeConstraintTagName)(block.blockTag.tagName.substring(1));
874
+ const parsedTag = nextParsedTag(tagName);
554
875
  if (tagName === "displayName" || tagName === "description" || tagName === "format" || tagName === "placeholder") {
555
- const text2 = extractBlockText(block).trim();
876
+ const text2 = getBestBlockPayloadText(parsedTag, commentText, range.pos, block);
556
877
  if (text2 === "") continue;
557
- const provenance2 = provenanceForComment(range, sourceFile, file, tagName);
878
+ const provenance2 = parsedTag !== null ? provenanceForParsedTag(parsedTag, sourceFile, file) : provenanceForComment(range, sourceFile, file, tagName);
558
879
  switch (tagName) {
559
880
  case "displayName":
560
881
  if (!isMemberTargetDisplayName(text2) && displayName === void 0) {
@@ -584,11 +905,29 @@ function parseTSDocTags(node, file = "", options) {
584
905
  continue;
585
906
  }
586
907
  if (TAGS_REQUIRING_RAW_TEXT.has(tagName)) continue;
587
- const text = extractBlockText(block).trim();
908
+ const text = getBestBlockPayloadText(parsedTag, commentText, range.pos, block);
588
909
  const expectedType = (0, import_core3.isBuiltinConstraintName)(tagName) ? import_core3.BUILTIN_CONSTRAINT_DEFINITIONS[tagName] : void 0;
589
910
  if (text === "" && expectedType !== "boolean") continue;
590
- const provenance = provenanceForComment(range, sourceFile, file, tagName);
591
- const constraintNode = parseConstraintValue(tagName, text, provenance, options);
911
+ const provenance = parsedTag !== null ? provenanceForParsedTag(parsedTag, sourceFile, file) : provenanceForComment(range, sourceFile, file, tagName);
912
+ const compilerDiagnostics = buildCompilerBackedConstraintDiagnostics(
913
+ node,
914
+ sourceFile,
915
+ tagName,
916
+ parsedTag,
917
+ provenance,
918
+ supportingDeclarations,
919
+ options
920
+ );
921
+ if (compilerDiagnostics.length > 0) {
922
+ diagnostics.push(...compilerDiagnostics);
923
+ continue;
924
+ }
925
+ const constraintNode = (0, import_analysis.parseConstraintTagValue)(
926
+ tagName,
927
+ text,
928
+ provenance,
929
+ sharedTagValueOptions(options)
930
+ );
592
931
  if (constraintNode) {
593
932
  constraints.push(constraintNode);
594
933
  }
@@ -642,57 +981,114 @@ function parseTSDocTags(node, file = "", options) {
642
981
  provenance: placeholderProvenance
643
982
  });
644
983
  }
645
- const jsDocTagsAll = ts.getJSDocTags(node);
646
- for (const tag of jsDocTagsAll) {
647
- const tagName = (0, import_core3.normalizeConstraintTagName)(tag.tagName.text);
648
- if (!TAGS_REQUIRING_RAW_TEXT.has(tagName)) continue;
649
- const commentText = getTagCommentText(tag);
650
- if (commentText === void 0 || commentText.trim() === "") continue;
651
- const text = commentText.trim();
652
- const provenance = provenanceForJSDocTag(tag, file);
653
- if (tagName === "defaultValue") {
654
- const defaultValueNode = parseDefaultValueValue(text, provenance);
655
- annotations.push(defaultValueNode);
656
- continue;
984
+ if (rawTextTags.length > 0) {
985
+ for (const rawTextTag of rawTextTags) {
986
+ const fallbackQueue = rawTextFallbacks.get(rawTextTag.tag.normalizedTagName);
987
+ const fallback = fallbackQueue?.shift();
988
+ const text = choosePreferredPayloadText(
989
+ getSharedPayloadText(rawTextTag.tag, rawTextTag.commentText, rawTextTag.commentOffset),
990
+ fallback?.text ?? ""
991
+ );
992
+ if (text === "") continue;
993
+ const provenance = provenanceForParsedTag(rawTextTag.tag, sourceFile, file);
994
+ if (rawTextTag.tag.normalizedTagName === "defaultValue") {
995
+ const defaultValueNode = (0, import_analysis.parseDefaultValueTagValue)(text, provenance);
996
+ annotations.push(defaultValueNode);
997
+ continue;
998
+ }
999
+ const compilerDiagnostics = buildCompilerBackedConstraintDiagnostics(
1000
+ node,
1001
+ sourceFile,
1002
+ rawTextTag.tag.normalizedTagName,
1003
+ rawTextTag.tag,
1004
+ provenance,
1005
+ supportingDeclarations,
1006
+ options
1007
+ );
1008
+ if (compilerDiagnostics.length > 0) {
1009
+ diagnostics.push(...compilerDiagnostics);
1010
+ continue;
1011
+ }
1012
+ const constraintNode = (0, import_analysis.parseConstraintTagValue)(
1013
+ rawTextTag.tag.normalizedTagName,
1014
+ text,
1015
+ provenance,
1016
+ sharedTagValueOptions(options)
1017
+ );
1018
+ if (constraintNode) {
1019
+ constraints.push(constraintNode);
1020
+ }
657
1021
  }
658
- const constraintNode = parseConstraintValue(tagName, text, provenance, options);
659
- if (constraintNode) {
660
- constraints.push(constraintNode);
1022
+ }
1023
+ for (const [tagName, fallbacks] of rawTextFallbacks) {
1024
+ for (const fallback of fallbacks) {
1025
+ const text = fallback.text.trim();
1026
+ if (text === "") continue;
1027
+ const provenance = fallback.provenance;
1028
+ if (tagName === "defaultValue") {
1029
+ const defaultValueNode = (0, import_analysis.parseDefaultValueTagValue)(text, provenance);
1030
+ annotations.push(defaultValueNode);
1031
+ continue;
1032
+ }
1033
+ const compilerDiagnostics = buildCompilerBackedConstraintDiagnostics(
1034
+ node,
1035
+ sourceFile,
1036
+ tagName,
1037
+ null,
1038
+ provenance,
1039
+ supportingDeclarations,
1040
+ options
1041
+ );
1042
+ if (compilerDiagnostics.length > 0) {
1043
+ diagnostics.push(...compilerDiagnostics);
1044
+ continue;
1045
+ }
1046
+ const constraintNode = (0, import_analysis.parseConstraintTagValue)(
1047
+ tagName,
1048
+ text,
1049
+ provenance,
1050
+ sharedTagValueOptions(options)
1051
+ );
1052
+ if (constraintNode) {
1053
+ constraints.push(constraintNode);
1054
+ }
661
1055
  }
662
1056
  }
663
- return { constraints, annotations };
1057
+ const result = { constraints, annotations, diagnostics };
1058
+ parseResultCache.set(cacheKey, result);
1059
+ return result;
664
1060
  }
665
1061
  function extractDisplayNameMetadata(node) {
666
1062
  let displayName;
667
1063
  const memberDisplayNames = /* @__PURE__ */ new Map();
668
- for (const tag of ts.getJSDocTags(node)) {
669
- const tagName = (0, import_core3.normalizeConstraintTagName)(tag.tagName.text);
670
- if (tagName !== "displayName") continue;
671
- const commentText = getTagCommentText(tag);
672
- if (commentText === void 0) continue;
673
- const text = commentText.trim();
674
- if (text === "") continue;
675
- const memberTarget = parseMemberTargetDisplayName(text);
676
- if (memberTarget) {
677
- memberDisplayNames.set(memberTarget.target, memberTarget.label);
678
- continue;
679
- }
680
- displayName ??= text;
1064
+ const sourceFile = node.getSourceFile();
1065
+ const sourceText = sourceFile.getFullText();
1066
+ const commentRanges = ts.getLeadingCommentRanges(sourceText, node.getFullStart());
1067
+ if (commentRanges) {
1068
+ for (const range of commentRanges) {
1069
+ if (range.kind !== ts.SyntaxKind.MultiLineCommentTrivia) continue;
1070
+ const commentText = sourceText.substring(range.pos, range.end);
1071
+ if (!commentText.startsWith("/**")) continue;
1072
+ const parsed = (0, import_analysis.parseCommentBlock)(commentText);
1073
+ for (const tag of parsed.tags) {
1074
+ if (tag.normalizedTagName !== "displayName") {
1075
+ continue;
1076
+ }
1077
+ if (tag.target !== null && tag.argumentText !== "") {
1078
+ memberDisplayNames.set(tag.target.rawText, tag.argumentText);
1079
+ continue;
1080
+ }
1081
+ if (tag.argumentText !== "") {
1082
+ displayName ??= tag.argumentText;
1083
+ }
1084
+ }
1085
+ }
681
1086
  }
682
1087
  return {
683
1088
  ...displayName !== void 0 && { displayName },
684
1089
  memberDisplayNames
685
1090
  };
686
1091
  }
687
- function extractPathTarget(text) {
688
- const trimmed = text.trimStart();
689
- const match = /^:([a-zA-Z_]\w*)(?:\s+([\s\S]*))?$/.exec(trimmed);
690
- if (!match?.[1]) return null;
691
- return {
692
- path: { segments: [match[1]] },
693
- remainingText: match[2] ?? ""
694
- };
695
- }
696
1092
  function extractBlockText(block) {
697
1093
  return extractPlainText(block.content);
698
1094
  }
@@ -711,219 +1107,48 @@ function extractPlainText(node) {
711
1107
  }
712
1108
  return result;
713
1109
  }
714
- function parseConstraintValue(tagName, text, provenance, options) {
715
- const customConstraint = parseExtensionConstraintValue(tagName, text, provenance, options);
716
- if (customConstraint) {
717
- return customConstraint;
718
- }
719
- if (!(0, import_core3.isBuiltinConstraintName)(tagName)) {
720
- return null;
721
- }
722
- const pathResult = extractPathTarget(text);
723
- const effectiveText = pathResult ? pathResult.remainingText : text;
724
- const path2 = pathResult?.path;
725
- const expectedType = import_core3.BUILTIN_CONSTRAINT_DEFINITIONS[tagName];
726
- if (expectedType === "number") {
727
- const value = Number(effectiveText);
728
- if (Number.isNaN(value)) {
729
- return null;
730
- }
731
- const numericKind = NUMERIC_CONSTRAINT_MAP[tagName];
732
- if (numericKind) {
733
- return {
734
- kind: "constraint",
735
- constraintKind: numericKind,
736
- value,
737
- ...path2 && { path: path2 },
738
- provenance
739
- };
740
- }
741
- const lengthKind = LENGTH_CONSTRAINT_MAP[tagName];
742
- if (lengthKind) {
743
- return {
744
- kind: "constraint",
745
- constraintKind: lengthKind,
746
- value,
747
- ...path2 && { path: path2 },
748
- provenance
749
- };
750
- }
751
- return null;
752
- }
753
- if (expectedType === "boolean") {
754
- const trimmed = effectiveText.trim();
755
- if (trimmed !== "" && trimmed !== "true") {
756
- return null;
757
- }
758
- if (tagName === "uniqueItems") {
759
- return {
760
- kind: "constraint",
761
- constraintKind: "uniqueItems",
762
- value: true,
763
- ...path2 && { path: path2 },
764
- provenance
765
- };
766
- }
767
- return null;
1110
+ function choosePreferredPayloadText(primary, fallback) {
1111
+ const preferred = primary.trim();
1112
+ const alternate = fallback.trim();
1113
+ if (preferred === "") return alternate;
1114
+ if (alternate === "") return preferred;
1115
+ if (alternate.includes("\n")) return alternate;
1116
+ if (alternate.length > preferred.length && alternate.startsWith(preferred)) {
1117
+ return alternate;
768
1118
  }
769
- if (expectedType === "json") {
770
- if (tagName === "const") {
771
- const trimmedText = effectiveText.trim();
772
- if (trimmedText === "") return null;
773
- try {
774
- const parsed2 = JSON.parse(trimmedText);
775
- return {
776
- kind: "constraint",
777
- constraintKind: "const",
778
- value: parsed2,
779
- ...path2 && { path: path2 },
780
- provenance
781
- };
782
- } catch {
783
- return {
784
- kind: "constraint",
785
- constraintKind: "const",
786
- value: trimmedText,
787
- ...path2 && { path: path2 },
788
- provenance
789
- };
790
- }
791
- }
792
- const parsed = tryParseJson(effectiveText);
793
- if (!Array.isArray(parsed)) {
794
- return null;
795
- }
796
- const members = [];
797
- for (const item of parsed) {
798
- if (typeof item === "string" || typeof item === "number") {
799
- members.push(item);
800
- } else if (typeof item === "object" && item !== null && "id" in item) {
801
- const id = item["id"];
802
- if (typeof id === "string" || typeof id === "number") {
803
- members.push(id);
804
- }
805
- }
806
- }
807
- return {
808
- kind: "constraint",
809
- constraintKind: "allowedMembers",
810
- members,
811
- ...path2 && { path: path2 },
812
- provenance
813
- };
814
- }
815
- return {
816
- kind: "constraint",
817
- constraintKind: "pattern",
818
- pattern: effectiveText,
819
- ...path2 && { path: path2 },
820
- provenance
821
- };
1119
+ return preferred;
822
1120
  }
823
- function parseExtensionConstraintValue(tagName, text, provenance, options) {
824
- const pathResult = extractPathTarget(text);
825
- const effectiveText = pathResult ? pathResult.remainingText : text;
826
- const path2 = pathResult?.path;
827
- const registry = options?.extensionRegistry;
828
- if (registry === void 0) {
829
- return null;
830
- }
831
- const directTag = registry.findConstraintTag(tagName);
832
- if (directTag !== void 0) {
833
- return makeCustomConstraintNode(
834
- directTag.extensionId,
835
- directTag.registration.constraintName,
836
- directTag.registration.parseValue(effectiveText),
837
- provenance,
838
- path2,
839
- registry
840
- );
841
- }
842
- if (!(0, import_core3.isBuiltinConstraintName)(tagName)) {
843
- return null;
1121
+ function getSharedPayloadText(tag, commentText, commentOffset) {
1122
+ if (tag.payloadSpan === null) {
1123
+ return "";
844
1124
  }
845
- const broadenedTypeId = getBroadenedCustomTypeId(options?.fieldType);
846
- if (broadenedTypeId === void 0) {
847
- return null;
848
- }
849
- const broadened = registry.findBuiltinConstraintBroadening(broadenedTypeId, tagName);
850
- if (broadened === void 0) {
851
- return null;
852
- }
853
- return makeCustomConstraintNode(
854
- broadened.extensionId,
855
- broadened.registration.constraintName,
856
- broadened.registration.parseValue(effectiveText),
857
- provenance,
858
- path2,
859
- registry
860
- );
1125
+ return (0, import_analysis.sliceCommentSpan)(commentText, tag.payloadSpan, {
1126
+ offset: commentOffset
1127
+ }).trim();
861
1128
  }
862
- function getBroadenedCustomTypeId(fieldType) {
863
- if (fieldType?.kind === "custom") {
864
- return fieldType.typeId;
865
- }
866
- if (fieldType?.kind !== "union") {
867
- return void 0;
868
- }
869
- const customMembers = fieldType.members.filter(
870
- (member) => member.kind === "custom"
871
- );
872
- if (customMembers.length !== 1) {
873
- return void 0;
874
- }
875
- const nonCustomMembers = fieldType.members.filter((member) => member.kind !== "custom");
876
- const allOtherMembersAreNull = nonCustomMembers.every(
877
- (member) => member.kind === "primitive" && member.primitiveKind === "null"
878
- );
879
- const customMember = customMembers[0];
880
- return allOtherMembersAreNull && customMember !== void 0 ? customMember.typeId : void 0;
1129
+ function getBestBlockPayloadText(tag, commentText, commentOffset, block) {
1130
+ const sharedText = tag === null ? "" : getSharedPayloadText(tag, commentText, commentOffset);
1131
+ const blockText = extractBlockText(block).replace(/\s+/g, " ").trim();
1132
+ return choosePreferredPayloadText(sharedText, blockText);
881
1133
  }
882
- function makeCustomConstraintNode(extensionId, constraintName, payload, provenance, path2, registry) {
883
- const constraintId = `${extensionId}/${constraintName}`;
884
- const registration = registry.findConstraint(constraintId);
885
- if (registration === void 0) {
886
- throw new Error(
887
- `Custom TSDoc tag resolved to unregistered constraint "${constraintId}". Register the constraint before using its tag.`
888
- );
889
- }
890
- return {
891
- kind: "constraint",
892
- constraintKind: "custom",
893
- constraintId,
894
- payload,
895
- compositionRule: registration.compositionRule,
896
- ...path2 && { path: path2 },
897
- provenance
898
- };
899
- }
900
- function parseDefaultValueValue(text, provenance) {
901
- const trimmed = text.trim();
902
- let value;
903
- if (trimmed === "null") {
904
- value = null;
905
- } else if (trimmed === "true") {
906
- value = true;
907
- } else if (trimmed === "false") {
908
- value = false;
909
- } else {
910
- const parsed = tryParseJson(trimmed);
911
- value = parsed !== null ? parsed : trimmed;
1134
+ function collectRawTextFallbacks(node, file) {
1135
+ const fallbacks = /* @__PURE__ */ new Map();
1136
+ for (const tag of ts.getJSDocTags(node)) {
1137
+ const tagName = (0, import_core3.normalizeConstraintTagName)(tag.tagName.text);
1138
+ if (!TAGS_REQUIRING_RAW_TEXT.has(tagName)) continue;
1139
+ const commentText = getTagCommentText(tag)?.trim() ?? "";
1140
+ if (commentText === "") continue;
1141
+ const entries = fallbacks.get(tagName) ?? [];
1142
+ entries.push({
1143
+ text: commentText,
1144
+ provenance: provenanceForJSDocTag(tag, file)
1145
+ });
1146
+ fallbacks.set(tagName, entries);
912
1147
  }
913
- return {
914
- kind: "annotation",
915
- annotationKind: "defaultValue",
916
- value,
917
- provenance
918
- };
1148
+ return fallbacks;
919
1149
  }
920
1150
  function isMemberTargetDisplayName(text) {
921
- return parseMemberTargetDisplayName(text) !== null;
922
- }
923
- function parseMemberTargetDisplayName(text) {
924
- const match = /^:([^\s]+)\s+([\s\S]+)$/.exec(text);
925
- if (!match?.[1] || !match[2]) return null;
926
- return { target: match[1], label: match[2].trim() };
1151
+ return (0, import_analysis.parseTagSyntax)("displayName", text).target !== null;
927
1152
  }
928
1153
  function provenanceForComment(range, sourceFile, file, tagName) {
929
1154
  const { line, character } = sourceFile.getLineAndCharacterOfPosition(range.pos);
@@ -935,6 +1160,16 @@ function provenanceForComment(range, sourceFile, file, tagName) {
935
1160
  tagName: "@" + tagName
936
1161
  };
937
1162
  }
1163
+ function provenanceForParsedTag(tag, sourceFile, file) {
1164
+ const { line, character } = sourceFile.getLineAndCharacterOfPosition(tag.tagNameSpan.start);
1165
+ return {
1166
+ surface: "tsdoc",
1167
+ file,
1168
+ line: line + 1,
1169
+ column: character,
1170
+ tagName: "@" + tag.normalizedTagName
1171
+ };
1172
+ }
938
1173
  function provenanceForJSDocTag(tag, file) {
939
1174
  const sourceFile = tag.getSourceFile();
940
1175
  const { line, character } = sourceFile.getLineAndCharacterOfPosition(tag.getStart());
@@ -957,12 +1192,15 @@ function getTagCommentText(tag) {
957
1192
  }
958
1193
 
959
1194
  // src/analyzer/jsdoc-constraints.ts
1195
+ function extractJSDocParseResult(node, file = "", options) {
1196
+ return parseTSDocTags(node, file, options);
1197
+ }
960
1198
  function extractJSDocConstraintNodes(node, file = "", options) {
961
- const result = parseTSDocTags(node, file, options);
1199
+ const result = extractJSDocParseResult(node, file, options);
962
1200
  return [...result.constraints];
963
1201
  }
964
1202
  function extractJSDocAnnotationNodes(node, file = "", options) {
965
- const result = parseTSDocTags(node, file, options);
1203
+ const result = extractJSDocParseResult(node, file, options);
966
1204
  return [...result.annotations];
967
1205
  }
968
1206
  function extractDefaultValueAnnotation(initializer, file = "") {
@@ -1011,13 +1249,16 @@ var RESOLVING_TYPE_PLACEHOLDER = {
1011
1249
  properties: [],
1012
1250
  additionalProperties: true
1013
1251
  };
1014
- function makeParseOptions(extensionRegistry, fieldType) {
1015
- if (extensionRegistry === void 0 && fieldType === void 0) {
1252
+ function makeParseOptions(extensionRegistry, fieldType, checker, subjectType, hostType) {
1253
+ if (extensionRegistry === void 0 && fieldType === void 0 && checker === void 0 && subjectType === void 0 && hostType === void 0) {
1016
1254
  return void 0;
1017
1255
  }
1018
1256
  return {
1019
1257
  ...extensionRegistry !== void 0 && { extensionRegistry },
1020
- ...fieldType !== void 0 && { fieldType }
1258
+ ...fieldType !== void 0 && { fieldType },
1259
+ ...checker !== void 0 && { checker },
1260
+ ...subjectType !== void 0 && { subjectType },
1261
+ ...hostType !== void 0 && { hostType }
1021
1262
  };
1022
1263
  }
1023
1264
  function analyzeClassToIR(classDecl, checker, file = "", extensionRegistry) {
@@ -1025,11 +1266,15 @@ function analyzeClassToIR(classDecl, checker, file = "", extensionRegistry) {
1025
1266
  const fields = [];
1026
1267
  const fieldLayouts = [];
1027
1268
  const typeRegistry = {};
1028
- const annotations = extractJSDocAnnotationNodes(
1269
+ const diagnostics = [];
1270
+ const classType = checker.getTypeAtLocation(classDecl);
1271
+ const classDoc = extractJSDocParseResult(
1029
1272
  classDecl,
1030
1273
  file,
1031
- makeParseOptions(extensionRegistry)
1274
+ makeParseOptions(extensionRegistry, void 0, checker, classType, classType)
1032
1275
  );
1276
+ const annotations = [...classDoc.annotations];
1277
+ diagnostics.push(...classDoc.diagnostics);
1033
1278
  const visiting = /* @__PURE__ */ new Set();
1034
1279
  const instanceMethods = [];
1035
1280
  const staticMethods = [];
@@ -1041,6 +1286,8 @@ function analyzeClassToIR(classDecl, checker, file = "", extensionRegistry) {
1041
1286
  file,
1042
1287
  typeRegistry,
1043
1288
  visiting,
1289
+ diagnostics,
1290
+ classType,
1044
1291
  extensionRegistry
1045
1292
  );
1046
1293
  if (fieldNode) {
@@ -1065,6 +1312,7 @@ function analyzeClassToIR(classDecl, checker, file = "", extensionRegistry) {
1065
1312
  fieldLayouts,
1066
1313
  typeRegistry,
1067
1314
  ...annotations.length > 0 && { annotations },
1315
+ ...diagnostics.length > 0 && { diagnostics },
1068
1316
  instanceMethods,
1069
1317
  staticMethods
1070
1318
  };
@@ -1073,11 +1321,15 @@ function analyzeInterfaceToIR(interfaceDecl, checker, file = "", extensionRegist
1073
1321
  const name = interfaceDecl.name.text;
1074
1322
  const fields = [];
1075
1323
  const typeRegistry = {};
1076
- const annotations = extractJSDocAnnotationNodes(
1324
+ const diagnostics = [];
1325
+ const interfaceType = checker.getTypeAtLocation(interfaceDecl);
1326
+ const interfaceDoc = extractJSDocParseResult(
1077
1327
  interfaceDecl,
1078
1328
  file,
1079
- makeParseOptions(extensionRegistry)
1329
+ makeParseOptions(extensionRegistry, void 0, checker, interfaceType, interfaceType)
1080
1330
  );
1331
+ const annotations = [...interfaceDoc.annotations];
1332
+ diagnostics.push(...interfaceDoc.diagnostics);
1081
1333
  const visiting = /* @__PURE__ */ new Set();
1082
1334
  for (const member of interfaceDecl.members) {
1083
1335
  if (ts3.isPropertySignature(member)) {
@@ -1087,6 +1339,8 @@ function analyzeInterfaceToIR(interfaceDecl, checker, file = "", extensionRegist
1087
1339
  file,
1088
1340
  typeRegistry,
1089
1341
  visiting,
1342
+ diagnostics,
1343
+ interfaceType,
1090
1344
  extensionRegistry
1091
1345
  );
1092
1346
  if (fieldNode) {
@@ -1101,6 +1355,7 @@ function analyzeInterfaceToIR(interfaceDecl, checker, file = "", extensionRegist
1101
1355
  fieldLayouts,
1102
1356
  typeRegistry,
1103
1357
  ...annotations.length > 0 && { annotations },
1358
+ ...diagnostics.length > 0 && { diagnostics },
1104
1359
  instanceMethods: [],
1105
1360
  staticMethods: []
1106
1361
  };
@@ -1118,11 +1373,15 @@ function analyzeTypeAliasToIR(typeAlias, checker, file = "", extensionRegistry)
1118
1373
  const name = typeAlias.name.text;
1119
1374
  const fields = [];
1120
1375
  const typeRegistry = {};
1121
- const annotations = extractJSDocAnnotationNodes(
1376
+ const diagnostics = [];
1377
+ const aliasType = checker.getTypeAtLocation(typeAlias);
1378
+ const typeAliasDoc = extractJSDocParseResult(
1122
1379
  typeAlias,
1123
1380
  file,
1124
- makeParseOptions(extensionRegistry)
1381
+ makeParseOptions(extensionRegistry, void 0, checker, aliasType, aliasType)
1125
1382
  );
1383
+ const annotations = [...typeAliasDoc.annotations];
1384
+ diagnostics.push(...typeAliasDoc.diagnostics);
1126
1385
  const visiting = /* @__PURE__ */ new Set();
1127
1386
  for (const member of typeAlias.type.members) {
1128
1387
  if (ts3.isPropertySignature(member)) {
@@ -1132,6 +1391,8 @@ function analyzeTypeAliasToIR(typeAlias, checker, file = "", extensionRegistry)
1132
1391
  file,
1133
1392
  typeRegistry,
1134
1393
  visiting,
1394
+ diagnostics,
1395
+ aliasType,
1135
1396
  extensionRegistry
1136
1397
  );
1137
1398
  if (fieldNode) {
@@ -1147,12 +1408,13 @@ function analyzeTypeAliasToIR(typeAlias, checker, file = "", extensionRegistry)
1147
1408
  fieldLayouts: fields.map(() => ({})),
1148
1409
  typeRegistry,
1149
1410
  ...annotations.length > 0 && { annotations },
1411
+ ...diagnostics.length > 0 && { diagnostics },
1150
1412
  instanceMethods: [],
1151
1413
  staticMethods: []
1152
1414
  }
1153
1415
  };
1154
1416
  }
1155
- function analyzeFieldToIR(prop, checker, file, typeRegistry, visiting, extensionRegistry) {
1417
+ function analyzeFieldToIR(prop, checker, file, typeRegistry, visiting, diagnostics, hostType, extensionRegistry) {
1156
1418
  if (!ts3.isIdentifier(prop.name)) {
1157
1419
  return null;
1158
1420
  }
@@ -1167,7 +1429,8 @@ function analyzeFieldToIR(prop, checker, file, typeRegistry, visiting, extension
1167
1429
  typeRegistry,
1168
1430
  visiting,
1169
1431
  prop,
1170
- extensionRegistry
1432
+ extensionRegistry,
1433
+ diagnostics
1171
1434
  );
1172
1435
  const constraints = [];
1173
1436
  if (prop.type && !shouldEmitPrimitiveAliasDefinition(prop.type, checker)) {
@@ -1175,13 +1438,15 @@ function analyzeFieldToIR(prop, checker, file, typeRegistry, visiting, extension
1175
1438
  ...extractTypeAliasConstraintNodes(prop.type, checker, file, extensionRegistry)
1176
1439
  );
1177
1440
  }
1178
- constraints.push(
1179
- ...extractJSDocConstraintNodes(prop, file, makeParseOptions(extensionRegistry, type))
1441
+ const docResult = extractJSDocParseResult(
1442
+ prop,
1443
+ file,
1444
+ makeParseOptions(extensionRegistry, type, checker, tsType, hostType)
1180
1445
  );
1446
+ constraints.push(...docResult.constraints);
1447
+ diagnostics.push(...docResult.diagnostics);
1181
1448
  let annotations = [];
1182
- annotations.push(
1183
- ...extractJSDocAnnotationNodes(prop, file, makeParseOptions(extensionRegistry, type))
1184
- );
1449
+ annotations.push(...docResult.annotations);
1185
1450
  const defaultAnnotation = extractDefaultValueAnnotation(prop.initializer, file);
1186
1451
  if (defaultAnnotation && !annotations.some((a) => a.annotationKind === "defaultValue")) {
1187
1452
  annotations.push(defaultAnnotation);
@@ -1197,7 +1462,7 @@ function analyzeFieldToIR(prop, checker, file, typeRegistry, visiting, extension
1197
1462
  provenance
1198
1463
  };
1199
1464
  }
1200
- function analyzeInterfacePropertyToIR(prop, checker, file, typeRegistry, visiting, extensionRegistry) {
1465
+ function analyzeInterfacePropertyToIR(prop, checker, file, typeRegistry, visiting, diagnostics, hostType, extensionRegistry) {
1201
1466
  if (!ts3.isIdentifier(prop.name)) {
1202
1467
  return null;
1203
1468
  }
@@ -1212,7 +1477,8 @@ function analyzeInterfacePropertyToIR(prop, checker, file, typeRegistry, visitin
1212
1477
  typeRegistry,
1213
1478
  visiting,
1214
1479
  prop,
1215
- extensionRegistry
1480
+ extensionRegistry,
1481
+ diagnostics
1216
1482
  );
1217
1483
  const constraints = [];
1218
1484
  if (prop.type && !shouldEmitPrimitiveAliasDefinition(prop.type, checker)) {
@@ -1220,13 +1486,15 @@ function analyzeInterfacePropertyToIR(prop, checker, file, typeRegistry, visitin
1220
1486
  ...extractTypeAliasConstraintNodes(prop.type, checker, file, extensionRegistry)
1221
1487
  );
1222
1488
  }
1223
- constraints.push(
1224
- ...extractJSDocConstraintNodes(prop, file, makeParseOptions(extensionRegistry, type))
1489
+ const docResult = extractJSDocParseResult(
1490
+ prop,
1491
+ file,
1492
+ makeParseOptions(extensionRegistry, type, checker, tsType, hostType)
1225
1493
  );
1494
+ constraints.push(...docResult.constraints);
1495
+ diagnostics.push(...docResult.diagnostics);
1226
1496
  let annotations = [];
1227
- annotations.push(
1228
- ...extractJSDocAnnotationNodes(prop, file, makeParseOptions(extensionRegistry, type))
1229
- );
1497
+ annotations.push(...docResult.annotations);
1230
1498
  ({ type, annotations } = applyEnumMemberDisplayNames(type, annotations));
1231
1499
  return {
1232
1500
  kind: "field",
@@ -1355,7 +1623,7 @@ function getTypeNodeRegistrationName(typeNode) {
1355
1623
  }
1356
1624
  return null;
1357
1625
  }
1358
- function resolveTypeNode(type, checker, file, typeRegistry, visiting, sourceNode, extensionRegistry) {
1626
+ function resolveTypeNode(type, checker, file, typeRegistry, visiting, sourceNode, extensionRegistry, diagnostics) {
1359
1627
  const customType = resolveRegisteredCustomType(sourceNode, extensionRegistry, checker);
1360
1628
  if (customType) {
1361
1629
  return customType;
@@ -1367,7 +1635,8 @@ function resolveTypeNode(type, checker, file, typeRegistry, visiting, sourceNode
1367
1635
  typeRegistry,
1368
1636
  visiting,
1369
1637
  sourceNode,
1370
- extensionRegistry
1638
+ extensionRegistry,
1639
+ diagnostics
1371
1640
  );
1372
1641
  if (primitiveAlias) {
1373
1642
  return primitiveAlias;
@@ -1410,7 +1679,8 @@ function resolveTypeNode(type, checker, file, typeRegistry, visiting, sourceNode
1410
1679
  typeRegistry,
1411
1680
  visiting,
1412
1681
  sourceNode,
1413
- extensionRegistry
1682
+ extensionRegistry,
1683
+ diagnostics
1414
1684
  );
1415
1685
  }
1416
1686
  if (checker.isArrayType(type)) {
@@ -1421,15 +1691,24 @@ function resolveTypeNode(type, checker, file, typeRegistry, visiting, sourceNode
1421
1691
  typeRegistry,
1422
1692
  visiting,
1423
1693
  sourceNode,
1424
- extensionRegistry
1694
+ extensionRegistry,
1695
+ diagnostics
1425
1696
  );
1426
1697
  }
1427
1698
  if (isObjectType(type)) {
1428
- return resolveObjectType(type, checker, file, typeRegistry, visiting, extensionRegistry);
1699
+ return resolveObjectType(
1700
+ type,
1701
+ checker,
1702
+ file,
1703
+ typeRegistry,
1704
+ visiting,
1705
+ extensionRegistry,
1706
+ diagnostics
1707
+ );
1429
1708
  }
1430
1709
  return { kind: "primitive", primitiveKind: "string" };
1431
1710
  }
1432
- function tryResolveNamedPrimitiveAlias(type, checker, file, typeRegistry, visiting, sourceNode, extensionRegistry) {
1711
+ function tryResolveNamedPrimitiveAlias(type, checker, file, typeRegistry, visiting, sourceNode, extensionRegistry, diagnostics) {
1433
1712
  if (!(type.flags & (ts3.TypeFlags.String | ts3.TypeFlags.Number | ts3.TypeFlags.BigInt | ts3.TypeFlags.BigIntLiteral | ts3.TypeFlags.Boolean | ts3.TypeFlags.Null))) {
1434
1713
  return null;
1435
1714
  }
@@ -1457,7 +1736,8 @@ function tryResolveNamedPrimitiveAlias(type, checker, file, typeRegistry, visiti
1457
1736
  file,
1458
1737
  typeRegistry,
1459
1738
  visiting,
1460
- extensionRegistry
1739
+ extensionRegistry,
1740
+ diagnostics
1461
1741
  ),
1462
1742
  ...constraints.length > 0 && { constraints },
1463
1743
  ...annotations.length > 0 && { annotations },
@@ -1484,7 +1764,7 @@ function shouldEmitPrimitiveAliasDefinition(typeNode, checker) {
1484
1764
  const resolved = checker.getTypeFromTypeNode(aliasDecl.type);
1485
1765
  return !!(resolved.flags & (ts3.TypeFlags.String | ts3.TypeFlags.Number | ts3.TypeFlags.BigInt | ts3.TypeFlags.BigIntLiteral | ts3.TypeFlags.Boolean | ts3.TypeFlags.Null));
1486
1766
  }
1487
- function resolveAliasedPrimitiveTarget(type, checker, file, typeRegistry, visiting, extensionRegistry) {
1767
+ function resolveAliasedPrimitiveTarget(type, checker, file, typeRegistry, visiting, extensionRegistry, diagnostics) {
1488
1768
  const nestedAliasDecl = type.aliasSymbol?.declarations?.find(ts3.isTypeAliasDeclaration);
1489
1769
  if (nestedAliasDecl !== void 0) {
1490
1770
  return resolveAliasedPrimitiveTarget(
@@ -1493,12 +1773,22 @@ function resolveAliasedPrimitiveTarget(type, checker, file, typeRegistry, visiti
1493
1773
  file,
1494
1774
  typeRegistry,
1495
1775
  visiting,
1496
- extensionRegistry
1776
+ extensionRegistry,
1777
+ diagnostics
1497
1778
  );
1498
1779
  }
1499
- return resolveTypeNode(type, checker, file, typeRegistry, visiting, void 0, extensionRegistry);
1780
+ return resolveTypeNode(
1781
+ type,
1782
+ checker,
1783
+ file,
1784
+ typeRegistry,
1785
+ visiting,
1786
+ void 0,
1787
+ extensionRegistry,
1788
+ diagnostics
1789
+ );
1500
1790
  }
1501
- function resolveUnionType(type, checker, file, typeRegistry, visiting, sourceNode, extensionRegistry) {
1791
+ function resolveUnionType(type, checker, file, typeRegistry, visiting, sourceNode, extensionRegistry, diagnostics) {
1502
1792
  const typeName = getNamedTypeName(type);
1503
1793
  const namedDecl = getNamedTypeDeclaration(type);
1504
1794
  if (typeName && typeName in typeRegistry) {
@@ -1588,7 +1878,8 @@ function resolveUnionType(type, checker, file, typeRegistry, visiting, sourceNod
1588
1878
  typeRegistry,
1589
1879
  visiting,
1590
1880
  nonNullMembers[0].sourceNode ?? sourceNode,
1591
- extensionRegistry
1881
+ extensionRegistry,
1882
+ diagnostics
1592
1883
  );
1593
1884
  const result = hasNull ? {
1594
1885
  kind: "union",
@@ -1604,7 +1895,8 @@ function resolveUnionType(type, checker, file, typeRegistry, visiting, sourceNod
1604
1895
  typeRegistry,
1605
1896
  visiting,
1606
1897
  memberSourceNode ?? sourceNode,
1607
- extensionRegistry
1898
+ extensionRegistry,
1899
+ diagnostics
1608
1900
  )
1609
1901
  );
1610
1902
  if (hasNull) {
@@ -1612,7 +1904,7 @@ function resolveUnionType(type, checker, file, typeRegistry, visiting, sourceNod
1612
1904
  }
1613
1905
  return registerNamed({ kind: "union", members });
1614
1906
  }
1615
- function resolveArrayType(type, checker, file, typeRegistry, visiting, sourceNode, extensionRegistry) {
1907
+ function resolveArrayType(type, checker, file, typeRegistry, visiting, sourceNode, extensionRegistry, diagnostics) {
1616
1908
  const typeArgs = isTypeReference(type) ? type.typeArguments : void 0;
1617
1909
  const elementType = typeArgs?.[0];
1618
1910
  const elementSourceNode = extractArrayElementTypeNode(sourceNode, checker);
@@ -1623,11 +1915,12 @@ function resolveArrayType(type, checker, file, typeRegistry, visiting, sourceNod
1623
1915
  typeRegistry,
1624
1916
  visiting,
1625
1917
  elementSourceNode,
1626
- extensionRegistry
1918
+ extensionRegistry,
1919
+ diagnostics
1627
1920
  ) : { kind: "primitive", primitiveKind: "string" };
1628
1921
  return { kind: "array", items };
1629
1922
  }
1630
- function tryResolveRecordType(type, checker, file, typeRegistry, visiting, extensionRegistry) {
1923
+ function tryResolveRecordType(type, checker, file, typeRegistry, visiting, extensionRegistry, diagnostics) {
1631
1924
  if (type.getProperties().length > 0) {
1632
1925
  return null;
1633
1926
  }
@@ -1642,7 +1935,8 @@ function tryResolveRecordType(type, checker, file, typeRegistry, visiting, exten
1642
1935
  typeRegistry,
1643
1936
  visiting,
1644
1937
  void 0,
1645
- extensionRegistry
1938
+ extensionRegistry,
1939
+ diagnostics
1646
1940
  );
1647
1941
  return { kind: "record", valueType };
1648
1942
  }
@@ -1671,7 +1965,7 @@ function typeNodeContainsReference(type, targetName) {
1671
1965
  }
1672
1966
  }
1673
1967
  }
1674
- function resolveObjectType(type, checker, file, typeRegistry, visiting, extensionRegistry) {
1968
+ function resolveObjectType(type, checker, file, typeRegistry, visiting, extensionRegistry, diagnostics) {
1675
1969
  const typeName = getNamedTypeName(type);
1676
1970
  const namedTypeName = typeName ?? void 0;
1677
1971
  const namedDecl = getNamedTypeDeclaration(type);
@@ -1708,7 +2002,8 @@ function resolveObjectType(type, checker, file, typeRegistry, visiting, extensio
1708
2002
  file,
1709
2003
  typeRegistry,
1710
2004
  visiting,
1711
- extensionRegistry
2005
+ extensionRegistry,
2006
+ diagnostics
1712
2007
  );
1713
2008
  if (recordNode) {
1714
2009
  visiting.delete(type);
@@ -1736,6 +2031,7 @@ function resolveObjectType(type, checker, file, typeRegistry, visiting, extensio
1736
2031
  file,
1737
2032
  typeRegistry,
1738
2033
  visiting,
2034
+ diagnostics ?? [],
1739
2035
  extensionRegistry
1740
2036
  );
1741
2037
  for (const prop of type.getProperties()) {
@@ -1750,7 +2046,8 @@ function resolveObjectType(type, checker, file, typeRegistry, visiting, extensio
1750
2046
  typeRegistry,
1751
2047
  visiting,
1752
2048
  declaration,
1753
- extensionRegistry
2049
+ extensionRegistry,
2050
+ diagnostics
1754
2051
  );
1755
2052
  const fieldNodeInfo = fieldInfoMap?.get(prop.name);
1756
2053
  properties.push({
@@ -1780,7 +2077,7 @@ function resolveObjectType(type, checker, file, typeRegistry, visiting, extensio
1780
2077
  }
1781
2078
  return objectNode;
1782
2079
  }
1783
- function getNamedTypeFieldNodeInfoMap(type, checker, file, typeRegistry, visiting, extensionRegistry) {
2080
+ function getNamedTypeFieldNodeInfoMap(type, checker, file, typeRegistry, visiting, diagnostics, extensionRegistry) {
1784
2081
  const symbols = [type.getSymbol(), type.aliasSymbol].filter(
1785
2082
  (s) => s?.declarations != null && s.declarations.length > 0
1786
2083
  );
@@ -1790,6 +2087,7 @@ function getNamedTypeFieldNodeInfoMap(type, checker, file, typeRegistry, visitin
1790
2087
  const classDecl = declarations.find(ts3.isClassDeclaration);
1791
2088
  if (classDecl) {
1792
2089
  const map = /* @__PURE__ */ new Map();
2090
+ const hostType = checker.getTypeAtLocation(classDecl);
1793
2091
  for (const member of classDecl.members) {
1794
2092
  if (ts3.isPropertyDeclaration(member) && ts3.isIdentifier(member.name)) {
1795
2093
  const fieldNode = analyzeFieldToIR(
@@ -1798,6 +2096,8 @@ function getNamedTypeFieldNodeInfoMap(type, checker, file, typeRegistry, visitin
1798
2096
  file,
1799
2097
  typeRegistry,
1800
2098
  visiting,
2099
+ diagnostics,
2100
+ hostType,
1801
2101
  extensionRegistry
1802
2102
  );
1803
2103
  if (fieldNode) {
@@ -1819,6 +2119,8 @@ function getNamedTypeFieldNodeInfoMap(type, checker, file, typeRegistry, visitin
1819
2119
  file,
1820
2120
  typeRegistry,
1821
2121
  visiting,
2122
+ checker.getTypeAtLocation(interfaceDecl),
2123
+ diagnostics,
1822
2124
  extensionRegistry
1823
2125
  );
1824
2126
  }
@@ -1830,6 +2132,8 @@ function getNamedTypeFieldNodeInfoMap(type, checker, file, typeRegistry, visitin
1830
2132
  file,
1831
2133
  typeRegistry,
1832
2134
  visiting,
2135
+ checker.getTypeAtLocation(typeAliasDecl),
2136
+ diagnostics,
1833
2137
  extensionRegistry
1834
2138
  );
1835
2139
  }
@@ -1879,7 +2183,7 @@ function isNullishTypeNode(typeNode) {
1879
2183
  }
1880
2184
  return ts3.isLiteralTypeNode(typeNode) && (typeNode.literal.kind === ts3.SyntaxKind.NullKeyword || typeNode.literal.kind === ts3.SyntaxKind.UndefinedKeyword);
1881
2185
  }
1882
- function buildFieldNodeInfoMap(members, checker, file, typeRegistry, visiting, extensionRegistry) {
2186
+ function buildFieldNodeInfoMap(members, checker, file, typeRegistry, visiting, hostType, diagnostics, extensionRegistry) {
1883
2187
  const map = /* @__PURE__ */ new Map();
1884
2188
  for (const member of members) {
1885
2189
  if (ts3.isPropertySignature(member)) {
@@ -1889,6 +2193,8 @@ function buildFieldNodeInfoMap(members, checker, file, typeRegistry, visiting, e
1889
2193
  file,
1890
2194
  typeRegistry,
1891
2195
  visiting,
2196
+ diagnostics,
2197
+ hostType,
1892
2198
  extensionRegistry
1893
2199
  );
1894
2200
  if (fieldNode) {
@@ -2753,760 +3059,42 @@ function generateUiSchemaFromIR(ir) {
2753
3059
  }
2754
3060
 
2755
3061
  // src/validate/constraint-validator.ts
2756
- var import_core4 = require("@formspec/core");
2757
- function addContradiction(ctx, message, primary, related) {
2758
- ctx.diagnostics.push({
2759
- code: "CONTRADICTING_CONSTRAINTS",
2760
- message,
2761
- severity: "error",
2762
- primaryLocation: primary,
2763
- relatedLocations: [related]
2764
- });
2765
- }
2766
- function addTypeMismatch(ctx, message, primary) {
2767
- ctx.diagnostics.push({
2768
- code: "TYPE_MISMATCH",
2769
- message,
2770
- severity: "error",
2771
- primaryLocation: primary,
2772
- relatedLocations: []
2773
- });
2774
- }
2775
- function addUnknownExtension(ctx, message, primary) {
2776
- ctx.diagnostics.push({
2777
- code: "UNKNOWN_EXTENSION",
2778
- message,
2779
- severity: "warning",
2780
- primaryLocation: primary,
2781
- relatedLocations: []
2782
- });
2783
- }
2784
- function addUnknownPathTarget(ctx, message, primary) {
2785
- ctx.diagnostics.push({
2786
- code: "UNKNOWN_PATH_TARGET",
2787
- message,
2788
- severity: "error",
2789
- primaryLocation: primary,
2790
- relatedLocations: []
2791
- });
2792
- }
2793
- function addConstraintBroadening(ctx, message, primary, related) {
2794
- ctx.diagnostics.push({
2795
- code: "CONSTRAINT_BROADENING",
2796
- message,
2797
- severity: "error",
2798
- primaryLocation: primary,
2799
- relatedLocations: [related]
2800
- });
2801
- }
2802
- function getExtensionIdFromConstraintId(constraintId) {
2803
- const separator = constraintId.lastIndexOf("/");
2804
- if (separator <= 0) {
2805
- return null;
2806
- }
2807
- return constraintId.slice(0, separator);
2808
- }
2809
- function findNumeric(constraints, constraintKind) {
2810
- return constraints.find((c) => c.constraintKind === constraintKind);
2811
- }
2812
- function findLength(constraints, constraintKind) {
2813
- return constraints.find((c) => c.constraintKind === constraintKind);
2814
- }
2815
- function findAllowedMembers(constraints) {
2816
- return constraints.filter(
2817
- (c) => c.constraintKind === "allowedMembers"
2818
- );
2819
- }
2820
- function findConstConstraints(constraints) {
2821
- return constraints.filter(
2822
- (c) => c.constraintKind === "const"
2823
- );
2824
- }
2825
- function jsonValueEquals(left, right) {
2826
- if (left === right) {
2827
- return true;
2828
- }
2829
- if (Array.isArray(left) || Array.isArray(right)) {
2830
- if (!Array.isArray(left) || !Array.isArray(right) || left.length !== right.length) {
2831
- return false;
2832
- }
2833
- return left.every((item, index) => jsonValueEquals(item, right[index]));
2834
- }
2835
- if (isJsonObject(left) || isJsonObject(right)) {
2836
- if (!isJsonObject(left) || !isJsonObject(right)) {
2837
- return false;
2838
- }
2839
- const leftKeys = Object.keys(left).sort();
2840
- const rightKeys = Object.keys(right).sort();
2841
- if (leftKeys.length !== rightKeys.length) {
2842
- return false;
2843
- }
2844
- return leftKeys.every((key, index) => {
2845
- const rightKey = rightKeys[index];
2846
- if (rightKey !== key) {
2847
- return false;
2848
- }
2849
- const leftValue = left[key];
2850
- const rightValue = right[rightKey];
2851
- return leftValue !== void 0 && rightValue !== void 0 && jsonValueEquals(leftValue, rightValue);
2852
- });
2853
- }
2854
- return false;
2855
- }
2856
- function isJsonObject(value) {
2857
- return typeof value === "object" && value !== null && !Array.isArray(value);
2858
- }
2859
- function isOrderedBoundConstraint(constraint) {
2860
- 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";
2861
- }
2862
- function pathKey(constraint) {
2863
- return constraint.path?.segments.join(".") ?? "";
2864
- }
2865
- function orderedBoundFamily(kind) {
2866
- switch (kind) {
2867
- case "minimum":
2868
- case "exclusiveMinimum":
2869
- return "numeric-lower";
2870
- case "maximum":
2871
- case "exclusiveMaximum":
2872
- return "numeric-upper";
2873
- case "minLength":
2874
- return "minLength";
2875
- case "minItems":
2876
- return "minItems";
2877
- case "maxLength":
2878
- return "maxLength";
2879
- case "maxItems":
2880
- return "maxItems";
2881
- default: {
2882
- const _exhaustive = kind;
2883
- return _exhaustive;
2884
- }
2885
- }
2886
- }
2887
- function isNumericLowerKind(kind) {
2888
- return kind === "minimum" || kind === "exclusiveMinimum";
2889
- }
2890
- function isNumericUpperKind(kind) {
2891
- return kind === "maximum" || kind === "exclusiveMaximum";
2892
- }
2893
- function describeConstraintTag(constraint) {
2894
- return `@${constraint.constraintKind}`;
2895
- }
2896
- function compareConstraintStrength(current, previous) {
2897
- const family = orderedBoundFamily(current.constraintKind);
2898
- if (family === "numeric-lower") {
2899
- if (!isNumericLowerKind(current.constraintKind) || !isNumericLowerKind(previous.constraintKind)) {
2900
- throw new Error("numeric-lower family received non-numeric lower-bound constraint");
2901
- }
2902
- if (current.value !== previous.value) {
2903
- return current.value > previous.value ? 1 : -1;
2904
- }
2905
- if (current.constraintKind === "exclusiveMinimum" && previous.constraintKind === "minimum") {
2906
- return 1;
2907
- }
2908
- if (current.constraintKind === "minimum" && previous.constraintKind === "exclusiveMinimum") {
2909
- return -1;
2910
- }
2911
- return 0;
2912
- }
2913
- if (family === "numeric-upper") {
2914
- if (!isNumericUpperKind(current.constraintKind) || !isNumericUpperKind(previous.constraintKind)) {
2915
- throw new Error("numeric-upper family received non-numeric upper-bound constraint");
2916
- }
2917
- if (current.value !== previous.value) {
2918
- return current.value < previous.value ? 1 : -1;
2919
- }
2920
- if (current.constraintKind === "exclusiveMaximum" && previous.constraintKind === "maximum") {
2921
- return 1;
2922
- }
2923
- if (current.constraintKind === "maximum" && previous.constraintKind === "exclusiveMaximum") {
2924
- return -1;
2925
- }
2926
- return 0;
2927
- }
2928
- switch (family) {
2929
- case "minLength":
2930
- case "minItems":
2931
- if (current.value === previous.value) {
2932
- return 0;
2933
- }
2934
- return current.value > previous.value ? 1 : -1;
2935
- case "maxLength":
2936
- case "maxItems":
2937
- if (current.value === previous.value) {
2938
- return 0;
2939
- }
2940
- return current.value < previous.value ? 1 : -1;
2941
- default: {
2942
- const _exhaustive = family;
2943
- return _exhaustive;
2944
- }
2945
- }
2946
- }
2947
- function checkConstraintBroadening(ctx, fieldName, constraints) {
2948
- const strongestByKey = /* @__PURE__ */ new Map();
2949
- for (const constraint of constraints) {
2950
- if (!isOrderedBoundConstraint(constraint)) {
2951
- continue;
2952
- }
2953
- const key = `${orderedBoundFamily(constraint.constraintKind)}:${pathKey(constraint)}`;
2954
- const previous = strongestByKey.get(key);
2955
- if (previous === void 0) {
2956
- strongestByKey.set(key, constraint);
2957
- continue;
2958
- }
2959
- const strength = compareConstraintStrength(constraint, previous);
2960
- if (strength < 0) {
2961
- const displayFieldName = formatPathTargetFieldName(
2962
- fieldName,
2963
- constraint.path?.segments ?? []
2964
- );
2965
- addConstraintBroadening(
2966
- ctx,
2967
- `Field "${displayFieldName}": ${describeConstraintTag(constraint)} (${String(constraint.value)}) is broader than earlier ${describeConstraintTag(previous)} (${String(previous.value)}). Constraints can only narrow.`,
2968
- constraint.provenance,
2969
- previous.provenance
2970
- );
2971
- continue;
2972
- }
2973
- if (strength <= 0) {
2974
- continue;
2975
- }
2976
- strongestByKey.set(key, constraint);
2977
- }
2978
- }
2979
- function compareCustomConstraintStrength(current, previous) {
2980
- const order = current.comparePayloads(current.constraint.payload, previous.constraint.payload);
2981
- const equalPayloadTiebreaker = order === 0 ? compareSemanticInclusivity(current.role.inclusive, previous.role.inclusive) : order;
2982
- switch (current.role.bound) {
2983
- case "lower":
2984
- return equalPayloadTiebreaker;
2985
- case "upper":
2986
- return equalPayloadTiebreaker === 0 ? 0 : -equalPayloadTiebreaker;
2987
- case "exact":
2988
- return order === 0 ? 0 : Number.NaN;
2989
- default: {
2990
- const _exhaustive = current.role.bound;
2991
- return _exhaustive;
2992
- }
2993
- }
2994
- }
2995
- function compareSemanticInclusivity(currentInclusive, previousInclusive) {
2996
- if (currentInclusive === previousInclusive) {
2997
- return 0;
2998
- }
2999
- return currentInclusive ? -1 : 1;
3000
- }
3001
- function customConstraintsContradict(lower, upper) {
3002
- const order = lower.comparePayloads(lower.constraint.payload, upper.constraint.payload);
3003
- if (order > 0) {
3004
- return true;
3005
- }
3006
- if (order < 0) {
3007
- return false;
3008
- }
3009
- return !lower.role.inclusive || !upper.role.inclusive;
3010
- }
3011
- function describeCustomConstraintTag(constraint) {
3012
- return constraint.provenance.tagName ?? constraint.constraintId;
3013
- }
3014
- function checkCustomConstraintSemantics(ctx, fieldName, constraints) {
3015
- if (ctx.extensionRegistry === void 0) {
3016
- return;
3017
- }
3018
- const strongestByKey = /* @__PURE__ */ new Map();
3019
- const lowerByFamily = /* @__PURE__ */ new Map();
3020
- const upperByFamily = /* @__PURE__ */ new Map();
3021
- for (const constraint of constraints) {
3022
- if (constraint.constraintKind !== "custom") {
3023
- continue;
3024
- }
3025
- const registration = ctx.extensionRegistry.findConstraint(constraint.constraintId);
3026
- if (registration?.comparePayloads === void 0 || registration.semanticRole === void 0) {
3027
- continue;
3028
- }
3029
- const entry = {
3030
- constraint,
3031
- comparePayloads: registration.comparePayloads,
3032
- role: registration.semanticRole
3033
- };
3034
- const familyKey = `${registration.semanticRole.family}:${pathKey(constraint)}`;
3035
- const boundKey = `${familyKey}:${registration.semanticRole.bound}`;
3036
- const previous = strongestByKey.get(boundKey);
3037
- if (previous !== void 0) {
3038
- const strength = compareCustomConstraintStrength(entry, previous);
3039
- if (Number.isNaN(strength)) {
3040
- addContradiction(
3041
- ctx,
3042
- `Field "${formatPathTargetFieldName(fieldName, constraint.path?.segments ?? [])}": ${describeCustomConstraintTag(constraint)} conflicts with ${describeCustomConstraintTag(previous.constraint)}`,
3043
- constraint.provenance,
3044
- previous.constraint.provenance
3045
- );
3046
- continue;
3047
- }
3048
- if (strength < 0) {
3049
- addConstraintBroadening(
3050
- ctx,
3051
- `Field "${formatPathTargetFieldName(fieldName, constraint.path?.segments ?? [])}": ${describeCustomConstraintTag(constraint)} is broader than earlier ${describeCustomConstraintTag(previous.constraint)}. Constraints can only narrow.`,
3052
- constraint.provenance,
3053
- previous.constraint.provenance
3054
- );
3055
- continue;
3056
- }
3057
- if (strength > 0) {
3058
- strongestByKey.set(boundKey, entry);
3059
- }
3060
- } else {
3061
- strongestByKey.set(boundKey, entry);
3062
- }
3063
- if (registration.semanticRole.bound === "lower") {
3064
- lowerByFamily.set(familyKey, strongestByKey.get(boundKey) ?? entry);
3065
- } else if (registration.semanticRole.bound === "upper") {
3066
- upperByFamily.set(familyKey, strongestByKey.get(boundKey) ?? entry);
3067
- }
3068
- }
3069
- for (const [familyKey, lower] of lowerByFamily) {
3070
- const upper = upperByFamily.get(familyKey);
3071
- if (upper === void 0) {
3072
- continue;
3073
- }
3074
- if (!customConstraintsContradict(lower, upper)) {
3075
- continue;
3076
- }
3077
- addContradiction(
3078
- ctx,
3079
- `Field "${formatPathTargetFieldName(fieldName, lower.constraint.path?.segments ?? [])}": ${describeCustomConstraintTag(lower.constraint)} contradicts ${describeCustomConstraintTag(upper.constraint)}`,
3080
- lower.constraint.provenance,
3081
- upper.constraint.provenance
3082
- );
3083
- }
3084
- }
3085
- function checkNumericContradictions(ctx, fieldName, constraints) {
3086
- const min = findNumeric(constraints, "minimum");
3087
- const max = findNumeric(constraints, "maximum");
3088
- const exMin = findNumeric(constraints, "exclusiveMinimum");
3089
- const exMax = findNumeric(constraints, "exclusiveMaximum");
3090
- if (min !== void 0 && max !== void 0 && min.value > max.value) {
3091
- addContradiction(
3092
- ctx,
3093
- `Field "${fieldName}": minimum (${String(min.value)}) is greater than maximum (${String(max.value)})`,
3094
- min.provenance,
3095
- max.provenance
3096
- );
3097
- }
3098
- if (exMin !== void 0 && max !== void 0 && exMin.value >= max.value) {
3099
- addContradiction(
3100
- ctx,
3101
- `Field "${fieldName}": exclusiveMinimum (${String(exMin.value)}) is greater than or equal to maximum (${String(max.value)})`,
3102
- exMin.provenance,
3103
- max.provenance
3104
- );
3105
- }
3106
- if (min !== void 0 && exMax !== void 0 && min.value >= exMax.value) {
3107
- addContradiction(
3108
- ctx,
3109
- `Field "${fieldName}": minimum (${String(min.value)}) is greater than or equal to exclusiveMaximum (${String(exMax.value)})`,
3110
- min.provenance,
3111
- exMax.provenance
3112
- );
3113
- }
3114
- if (exMin !== void 0 && exMax !== void 0 && exMin.value >= exMax.value) {
3115
- addContradiction(
3116
- ctx,
3117
- `Field "${fieldName}": exclusiveMinimum (${String(exMin.value)}) is greater than or equal to exclusiveMaximum (${String(exMax.value)})`,
3118
- exMin.provenance,
3119
- exMax.provenance
3120
- );
3121
- }
3122
- }
3123
- function checkLengthContradictions(ctx, fieldName, constraints) {
3124
- const minLen = findLength(constraints, "minLength");
3125
- const maxLen = findLength(constraints, "maxLength");
3126
- if (minLen !== void 0 && maxLen !== void 0 && minLen.value > maxLen.value) {
3127
- addContradiction(
3128
- ctx,
3129
- `Field "${fieldName}": minLength (${String(minLen.value)}) is greater than maxLength (${String(maxLen.value)})`,
3130
- minLen.provenance,
3131
- maxLen.provenance
3132
- );
3133
- }
3134
- const minItems = findLength(constraints, "minItems");
3135
- const maxItems = findLength(constraints, "maxItems");
3136
- if (minItems !== void 0 && maxItems !== void 0 && minItems.value > maxItems.value) {
3137
- addContradiction(
3138
- ctx,
3139
- `Field "${fieldName}": minItems (${String(minItems.value)}) is greater than maxItems (${String(maxItems.value)})`,
3140
- minItems.provenance,
3141
- maxItems.provenance
3142
- );
3143
- }
3144
- }
3145
- function checkAllowedMembersContradiction(ctx, fieldName, constraints) {
3146
- const members = findAllowedMembers(constraints);
3147
- if (members.length < 2) return;
3148
- const firstSet = new Set(members[0]?.members ?? []);
3149
- for (let i = 1; i < members.length; i++) {
3150
- const current = members[i];
3151
- if (current === void 0) continue;
3152
- for (const m of firstSet) {
3153
- if (!current.members.includes(m)) {
3154
- firstSet.delete(m);
3155
- }
3156
- }
3157
- }
3158
- if (firstSet.size === 0) {
3159
- const first = members[0];
3160
- const second = members[1];
3161
- if (first !== void 0 && second !== void 0) {
3162
- addContradiction(
3163
- ctx,
3164
- `Field "${fieldName}": allowedMembers constraints have an empty intersection (no valid values remain)`,
3165
- first.provenance,
3166
- second.provenance
3167
- );
3168
- }
3169
- }
3170
- }
3171
- function checkConstContradictions(ctx, fieldName, constraints) {
3172
- const constConstraints = findConstConstraints(constraints);
3173
- if (constConstraints.length < 2) return;
3174
- const first = constConstraints[0];
3175
- if (first === void 0) return;
3176
- for (let i = 1; i < constConstraints.length; i++) {
3177
- const current = constConstraints[i];
3178
- if (current === void 0) continue;
3179
- if (jsonValueEquals(first.value, current.value)) {
3180
- continue;
3181
- }
3182
- addContradiction(
3183
- ctx,
3184
- `Field "${fieldName}": conflicting @const constraints require both ${JSON.stringify(first.value)} and ${JSON.stringify(current.value)}`,
3185
- first.provenance,
3186
- current.provenance
3187
- );
3188
- }
3189
- }
3190
- function typeLabel(type) {
3191
- switch (type.kind) {
3192
- case "primitive":
3193
- return type.primitiveKind;
3194
- case "enum":
3195
- return "enum";
3196
- case "array":
3197
- return "array";
3198
- case "object":
3199
- return "object";
3200
- case "record":
3201
- return "record";
3202
- case "union":
3203
- return "union";
3204
- case "reference":
3205
- return `reference(${type.name})`;
3206
- case "dynamic":
3207
- return `dynamic(${type.dynamicKind})`;
3208
- case "custom":
3209
- return `custom(${type.typeId})`;
3210
- default: {
3211
- const _exhaustive = type;
3212
- return String(_exhaustive);
3213
- }
3214
- }
3215
- }
3216
- function dereferenceType(ctx, type) {
3217
- let current = type;
3218
- const seen = /* @__PURE__ */ new Set();
3219
- while (current.kind === "reference") {
3220
- if (seen.has(current.name)) {
3221
- return current;
3222
- }
3223
- seen.add(current.name);
3224
- const definition = ctx.typeRegistry[current.name];
3225
- if (definition === void 0) {
3226
- return current;
3227
- }
3228
- current = definition.type;
3229
- }
3230
- return current;
3231
- }
3232
- function collectReferencedTypeConstraints(ctx, type) {
3233
- const collected = [];
3234
- let current = type;
3235
- const seen = /* @__PURE__ */ new Set();
3236
- while (current.kind === "reference") {
3237
- if (seen.has(current.name)) {
3238
- break;
3239
- }
3240
- seen.add(current.name);
3241
- const definition = ctx.typeRegistry[current.name];
3242
- if (definition === void 0) {
3243
- break;
3244
- }
3245
- if (definition.constraints !== void 0) {
3246
- collected.push(...definition.constraints);
3247
- }
3248
- current = definition.type;
3249
- }
3250
- return collected;
3251
- }
3252
- function resolvePathTargetType(ctx, type, segments) {
3253
- const effectiveType = dereferenceType(ctx, type);
3254
- if (segments.length === 0) {
3255
- return { kind: "resolved", type: effectiveType };
3256
- }
3257
- if (effectiveType.kind === "array") {
3258
- return resolvePathTargetType(ctx, effectiveType.items, segments);
3259
- }
3260
- if (effectiveType.kind === "object") {
3261
- const [segment, ...rest] = segments;
3262
- if (segment === void 0) {
3263
- throw new Error("Invariant violation: object path traversal requires a segment");
3264
- }
3265
- const property = effectiveType.properties.find((prop) => prop.name === segment);
3266
- if (property === void 0) {
3267
- return { kind: "missing-property", segment };
3268
- }
3269
- return resolvePathTargetType(ctx, property.type, rest);
3270
- }
3271
- return { kind: "unresolvable", type: effectiveType };
3272
- }
3273
- function isNullType(type) {
3274
- return type.kind === "primitive" && type.primitiveKind === "null";
3275
- }
3276
- function collectCustomConstraintCandidateTypes(ctx, type) {
3277
- const effectiveType = dereferenceType(ctx, type);
3278
- const candidates = [effectiveType];
3279
- if (effectiveType.kind === "array") {
3280
- candidates.push(...collectCustomConstraintCandidateTypes(ctx, effectiveType.items));
3281
- }
3282
- if (effectiveType.kind === "union") {
3283
- const memberTypes = effectiveType.members.map((member) => dereferenceType(ctx, member));
3284
- const nonNullMembers = memberTypes.filter((member) => !isNullType(member));
3285
- if (nonNullMembers.length === 1 && nonNullMembers.length < memberTypes.length) {
3286
- const [nullableMember] = nonNullMembers;
3287
- if (nullableMember !== void 0) {
3288
- candidates.push(...collectCustomConstraintCandidateTypes(ctx, nullableMember));
3289
- }
3290
- }
3291
- }
3292
- return candidates;
3293
- }
3294
- function formatPathTargetFieldName(fieldName, path2) {
3295
- return path2.length === 0 ? fieldName : `${fieldName}.${path2.join(".")}`;
3296
- }
3297
- function checkConstraintOnType(ctx, fieldName, type, constraint) {
3298
- const effectiveType = dereferenceType(ctx, type);
3299
- const isNumber = effectiveType.kind === "primitive" && ["number", "integer", "bigint"].includes(effectiveType.primitiveKind);
3300
- const isString = effectiveType.kind === "primitive" && effectiveType.primitiveKind === "string";
3301
- const isArray = effectiveType.kind === "array";
3302
- const isEnum = effectiveType.kind === "enum";
3303
- const arrayItemType = effectiveType.kind === "array" ? dereferenceType(ctx, effectiveType.items) : void 0;
3304
- const isStringArray = arrayItemType?.kind === "primitive" && arrayItemType.primitiveKind === "string";
3305
- const label = typeLabel(effectiveType);
3306
- const ck = constraint.constraintKind;
3307
- switch (ck) {
3308
- case "minimum":
3309
- case "maximum":
3310
- case "exclusiveMinimum":
3311
- case "exclusiveMaximum":
3312
- case "multipleOf": {
3313
- if (!isNumber) {
3314
- addTypeMismatch(
3315
- ctx,
3316
- `Field "${fieldName}": constraint "${ck}" is only valid on number fields, but field type is "${label}"`,
3317
- constraint.provenance
3318
- );
3319
- }
3320
- break;
3321
- }
3322
- case "minLength":
3323
- case "maxLength":
3324
- case "pattern": {
3325
- if (!isString && !isStringArray) {
3326
- addTypeMismatch(
3327
- ctx,
3328
- `Field "${fieldName}": constraint "${ck}" is only valid on string fields or string array items, but field type is "${label}"`,
3329
- constraint.provenance
3330
- );
3331
- }
3332
- break;
3333
- }
3334
- case "minItems":
3335
- case "maxItems":
3336
- case "uniqueItems": {
3337
- if (!isArray) {
3338
- addTypeMismatch(
3339
- ctx,
3340
- `Field "${fieldName}": constraint "${ck}" is only valid on array fields, but field type is "${label}"`,
3341
- constraint.provenance
3342
- );
3343
- }
3344
- break;
3345
- }
3346
- case "allowedMembers": {
3347
- if (!isEnum) {
3348
- addTypeMismatch(
3349
- ctx,
3350
- `Field "${fieldName}": constraint "allowedMembers" is only valid on enum fields, but field type is "${label}"`,
3351
- constraint.provenance
3352
- );
3353
- }
3354
- break;
3355
- }
3356
- case "const": {
3357
- const isPrimitiveConstType = effectiveType.kind === "primitive" && ["string", "number", "integer", "bigint", "boolean", "null"].includes(
3358
- effectiveType.primitiveKind
3359
- ) || effectiveType.kind === "enum";
3360
- if (!isPrimitiveConstType) {
3361
- addTypeMismatch(
3362
- ctx,
3363
- `Field "${fieldName}": constraint "const" is only valid on primitive or enum fields, but field type is "${label}"`,
3364
- constraint.provenance
3365
- );
3366
- break;
3367
- }
3368
- if (effectiveType.kind === "primitive") {
3369
- const valueType = constraint.value === null ? "null" : Array.isArray(constraint.value) ? "array" : typeof constraint.value;
3370
- const expectedValueType = effectiveType.primitiveKind === "integer" || effectiveType.primitiveKind === "bigint" ? "number" : effectiveType.primitiveKind;
3371
- if (valueType !== expectedValueType) {
3372
- addTypeMismatch(
3373
- ctx,
3374
- `Field "${fieldName}": @const value type "${valueType}" is incompatible with field type "${effectiveType.primitiveKind}"`,
3375
- constraint.provenance
3376
- );
3377
- }
3378
- break;
3379
- }
3380
- const memberValues = effectiveType.members.map((member) => member.value);
3381
- if (!memberValues.some((member) => jsonValueEquals(member, constraint.value))) {
3382
- addTypeMismatch(
3383
- ctx,
3384
- `Field "${fieldName}": @const value ${JSON.stringify(constraint.value)} is not one of the enum members`,
3385
- constraint.provenance
3386
- );
3387
- }
3388
- break;
3389
- }
3390
- case "custom": {
3391
- checkCustomConstraint(ctx, fieldName, effectiveType, constraint);
3392
- break;
3393
- }
3394
- default: {
3395
- const _exhaustive = constraint;
3396
- throw new Error(
3397
- `Unhandled constraint kind: ${_exhaustive.constraintKind}`
3398
- );
3399
- }
3400
- }
3401
- }
3402
- function checkTypeApplicability(ctx, fieldName, type, constraints) {
3403
- for (const constraint of constraints) {
3404
- if (constraint.path) {
3405
- const resolution = resolvePathTargetType(ctx, type, constraint.path.segments);
3406
- const targetFieldName = formatPathTargetFieldName(fieldName, constraint.path.segments);
3407
- if (resolution.kind === "missing-property") {
3408
- addUnknownPathTarget(
3409
- ctx,
3410
- `Field "${targetFieldName}": path-targeted constraint "${constraint.constraintKind}" references unknown path segment "${resolution.segment}"`,
3411
- constraint.provenance
3412
- );
3413
- continue;
3414
- }
3415
- if (resolution.kind === "unresolvable") {
3416
- addTypeMismatch(
3417
- ctx,
3418
- `Field "${targetFieldName}": path-targeted constraint "${constraint.constraintKind}" is invalid because type "${typeLabel(resolution.type)}" cannot be traversed`,
3419
- constraint.provenance
3420
- );
3421
- continue;
3422
- }
3423
- checkConstraintOnType(ctx, targetFieldName, resolution.type, constraint);
3424
- continue;
3425
- }
3426
- checkConstraintOnType(ctx, fieldName, type, constraint);
3427
- }
3428
- }
3429
- function checkCustomConstraint(ctx, fieldName, type, constraint) {
3430
- if (ctx.extensionRegistry === void 0) return;
3431
- const registration = ctx.extensionRegistry.findConstraint(constraint.constraintId);
3432
- if (registration === void 0) {
3433
- addUnknownExtension(
3434
- ctx,
3435
- `Field "${fieldName}": custom constraint "${constraint.constraintId}" is not registered in the extension registry`,
3436
- constraint.provenance
3437
- );
3438
- return;
3439
- }
3440
- const candidateTypes = collectCustomConstraintCandidateTypes(ctx, type);
3441
- const normalizedTagName = constraint.provenance.tagName === void 0 ? void 0 : (0, import_core4.normalizeConstraintTagName)(constraint.provenance.tagName.replace(/^@/, ""));
3442
- if (normalizedTagName !== void 0) {
3443
- const tagRegistration = ctx.extensionRegistry.findConstraintTag(normalizedTagName);
3444
- const extensionId = getExtensionIdFromConstraintId(constraint.constraintId);
3445
- if (extensionId !== null && tagRegistration?.extensionId === extensionId && tagRegistration.registration.constraintName === registration.constraintName && !candidateTypes.some(
3446
- (candidateType) => tagRegistration.registration.isApplicableToType?.(candidateType) !== false
3447
- )) {
3448
- addTypeMismatch(
3449
- ctx,
3450
- `Field "${fieldName}": custom constraint "${constraint.constraintId}" is not applicable to type "${typeLabel(type)}"`,
3451
- constraint.provenance
3452
- );
3453
- return;
3454
- }
3455
- }
3456
- if (registration.applicableTypes === null) {
3457
- if (!candidateTypes.some((candidateType) => registration.isApplicableToType?.(candidateType) !== false)) {
3458
- addTypeMismatch(
3459
- ctx,
3460
- `Field "${fieldName}": custom constraint "${constraint.constraintId}" is not applicable to type "${typeLabel(type)}"`,
3461
- constraint.provenance
3462
- );
3062
+ var import_analysis2 = require("@formspec/analysis");
3063
+ function validateFieldNode(ctx, field) {
3064
+ const analysis = (0, import_analysis2.analyzeConstraintTargets)(
3065
+ field.name,
3066
+ field.type,
3067
+ field.constraints,
3068
+ ctx.typeRegistry,
3069
+ ctx.extensionRegistry === void 0 ? void 0 : {
3070
+ extensionRegistry: ctx.extensionRegistry
3463
3071
  }
3464
- return;
3465
- }
3466
- const applicableTypes = registration.applicableTypes;
3467
- const matchesApplicableType = candidateTypes.some(
3468
- (candidateType) => applicableTypes.includes(candidateType.kind) && registration.isApplicableToType?.(candidateType) !== false
3469
3072
  );
3470
- if (!matchesApplicableType) {
3471
- addTypeMismatch(
3472
- ctx,
3473
- `Field "${fieldName}": custom constraint "${constraint.constraintId}" is not applicable to type "${typeLabel(type)}"`,
3474
- constraint.provenance
3475
- );
3476
- }
3477
- }
3478
- function validateFieldNode(ctx, field) {
3479
- validateConstraints(ctx, field.name, field.type, [
3480
- ...collectReferencedTypeConstraints(ctx, field.type),
3481
- ...field.constraints
3482
- ]);
3073
+ ctx.diagnostics.push(...analysis.diagnostics);
3483
3074
  if (field.type.kind === "object") {
3484
- for (const prop of field.type.properties) {
3485
- validateObjectProperty(ctx, field.name, prop);
3075
+ for (const property of field.type.properties) {
3076
+ validateObjectProperty(ctx, field.name, property);
3486
3077
  }
3487
3078
  }
3488
3079
  }
3489
- function validateObjectProperty(ctx, parentName, prop) {
3490
- const qualifiedName = `${parentName}.${prop.name}`;
3491
- validateConstraints(ctx, qualifiedName, prop.type, [
3492
- ...collectReferencedTypeConstraints(ctx, prop.type),
3493
- ...prop.constraints
3494
- ]);
3495
- if (prop.type.kind === "object") {
3496
- for (const nestedProp of prop.type.properties) {
3497
- validateObjectProperty(ctx, qualifiedName, nestedProp);
3080
+ function validateObjectProperty(ctx, parentName, property) {
3081
+ const qualifiedName = `${parentName}.${property.name}`;
3082
+ const analysis = (0, import_analysis2.analyzeConstraintTargets)(
3083
+ qualifiedName,
3084
+ property.type,
3085
+ property.constraints,
3086
+ ctx.typeRegistry,
3087
+ ctx.extensionRegistry === void 0 ? void 0 : {
3088
+ extensionRegistry: ctx.extensionRegistry
3089
+ }
3090
+ );
3091
+ ctx.diagnostics.push(...analysis.diagnostics);
3092
+ if (property.type.kind === "object") {
3093
+ for (const nestedProperty of property.type.properties) {
3094
+ validateObjectProperty(ctx, qualifiedName, nestedProperty);
3498
3095
  }
3499
3096
  }
3500
3097
  }
3501
- function validateConstraints(ctx, name, type, constraints) {
3502
- checkNumericContradictions(ctx, name, constraints);
3503
- checkLengthContradictions(ctx, name, constraints);
3504
- checkAllowedMembersContradiction(ctx, name, constraints);
3505
- checkConstContradictions(ctx, name, constraints);
3506
- checkConstraintBroadening(ctx, name, constraints);
3507
- checkCustomConstraintSemantics(ctx, name, constraints);
3508
- checkTypeApplicability(ctx, name, type, constraints);
3509
- }
3510
3098
  function validateElement(ctx, element) {
3511
3099
  switch (element.kind) {
3512
3100
  case "field":
@@ -3523,8 +3111,8 @@ function validateElement(ctx, element) {
3523
3111
  }
3524
3112
  break;
3525
3113
  default: {
3526
- const _exhaustive = element;
3527
- throw new Error(`Unhandled element kind: ${_exhaustive.kind}`);
3114
+ const exhaustive = element;
3115
+ throw new Error(`Unhandled element kind: ${String(exhaustive)}`);
3528
3116
  }
3529
3117
  }
3530
3118
  }
@@ -3539,12 +3127,18 @@ function validateIR(ir, options) {
3539
3127
  }
3540
3128
  return {
3541
3129
  diagnostics: ctx.diagnostics,
3542
- valid: ctx.diagnostics.every((d) => d.severity !== "error")
3130
+ valid: ctx.diagnostics.every((diagnostic) => diagnostic.severity !== "error")
3543
3131
  };
3544
3132
  }
3545
3133
 
3546
3134
  // src/generators/class-schema.ts
3547
3135
  function generateClassSchemas(analysis, source, options) {
3136
+ const errorDiagnostics = analysis.diagnostics?.filter(
3137
+ (diagnostic) => diagnostic.severity === "error"
3138
+ );
3139
+ if (errorDiagnostics !== void 0 && errorDiagnostics.length > 0) {
3140
+ throw new Error(formatValidationError(errorDiagnostics));
3141
+ }
3548
3142
  const ir = canonicalizeTSDoc(analysis, source);
3549
3143
  const validationResult = validateIR(ir, {
3550
3144
  ...options?.extensionRegistry !== void 0 && {
@@ -3654,7 +3248,7 @@ function createExtensionRegistry(extensions) {
3654
3248
  }
3655
3249
 
3656
3250
  // src/generators/method-schema.ts
3657
- var import_core5 = require("@formspec/core");
3251
+ var import_core4 = require("@formspec/core");
3658
3252
  function typeToJsonSchema(type, checker) {
3659
3253
  const typeRegistry = {};
3660
3254
  const visiting = /* @__PURE__ */ new Set();
@@ -3662,7 +3256,7 @@ function typeToJsonSchema(type, checker) {
3662
3256
  const fieldProvenance = { surface: "tsdoc", file: "", line: 0, column: 0 };
3663
3257
  const ir = {
3664
3258
  kind: "form-ir",
3665
- irVersion: import_core5.IR_VERSION,
3259
+ irVersion: import_core4.IR_VERSION,
3666
3260
  elements: [
3667
3261
  {
3668
3262
  kind: "field",