@formspec/build 0.1.0-alpha.27 → 0.1.0-alpha.28

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.
@@ -445,6 +445,7 @@ var path = __toESM(require("path"), 1);
445
445
 
446
446
  // src/analyzer/class-analyzer.ts
447
447
  var ts3 = __toESM(require("typescript"), 1);
448
+ var import_internal2 = require("@formspec/analysis/internal");
448
449
 
449
450
  // src/analyzer/jsdoc-constraints.ts
450
451
  var ts2 = __toESM(require("typescript"), 1);
@@ -1367,9 +1368,17 @@ function analyzeClassToIR(classDecl, checker, file = "", extensionRegistry) {
1367
1368
  }
1368
1369
  }
1369
1370
  }
1371
+ const specializedFields = applyDeclarationDiscriminatorToFields(
1372
+ fields,
1373
+ classDecl,
1374
+ classType,
1375
+ checker,
1376
+ file,
1377
+ diagnostics
1378
+ );
1370
1379
  return {
1371
1380
  name,
1372
- fields,
1381
+ fields: specializedFields,
1373
1382
  fieldLayouts,
1374
1383
  typeRegistry,
1375
1384
  ...annotations.length > 0 && { annotations },
@@ -1409,10 +1418,18 @@ function analyzeInterfaceToIR(interfaceDecl, checker, file = "", extensionRegist
1409
1418
  }
1410
1419
  }
1411
1420
  }
1412
- const fieldLayouts = fields.map(() => ({}));
1421
+ const specializedFields = applyDeclarationDiscriminatorToFields(
1422
+ fields,
1423
+ interfaceDecl,
1424
+ interfaceType,
1425
+ checker,
1426
+ file,
1427
+ diagnostics
1428
+ );
1429
+ const fieldLayouts = specializedFields.map(() => ({}));
1413
1430
  return {
1414
1431
  name,
1415
- fields,
1432
+ fields: specializedFields,
1416
1433
  fieldLayouts,
1417
1434
  typeRegistry,
1418
1435
  ...annotations.length > 0 && { annotations },
@@ -1461,12 +1478,20 @@ function analyzeTypeAliasToIR(typeAlias, checker, file = "", extensionRegistry)
1461
1478
  }
1462
1479
  }
1463
1480
  }
1481
+ const specializedFields = applyDeclarationDiscriminatorToFields(
1482
+ fields,
1483
+ typeAlias,
1484
+ aliasType,
1485
+ checker,
1486
+ file,
1487
+ diagnostics
1488
+ );
1464
1489
  return {
1465
1490
  ok: true,
1466
1491
  analysis: {
1467
1492
  name,
1468
- fields,
1469
- fieldLayouts: fields.map(() => ({})),
1493
+ fields: specializedFields,
1494
+ fieldLayouts: specializedFields.map(() => ({})),
1470
1495
  typeRegistry,
1471
1496
  ...annotations.length > 0 && { annotations },
1472
1497
  ...diagnostics.length > 0 && { diagnostics },
@@ -1475,6 +1500,396 @@ function analyzeTypeAliasToIR(typeAlias, checker, file = "", extensionRegistry)
1475
1500
  }
1476
1501
  };
1477
1502
  }
1503
+ function makeAnalysisDiagnostic(code, message, primaryLocation, relatedLocations = []) {
1504
+ return {
1505
+ code,
1506
+ message,
1507
+ severity: "error",
1508
+ primaryLocation,
1509
+ relatedLocations
1510
+ };
1511
+ }
1512
+ function getLeadingParsedTags(node) {
1513
+ const sourceFile = node.getSourceFile();
1514
+ const sourceText = sourceFile.getFullText();
1515
+ const commentRanges = ts3.getLeadingCommentRanges(sourceText, node.getFullStart());
1516
+ if (commentRanges === void 0) {
1517
+ return [];
1518
+ }
1519
+ const parsedTags = [];
1520
+ for (const range of commentRanges) {
1521
+ if (range.kind !== ts3.SyntaxKind.MultiLineCommentTrivia) {
1522
+ continue;
1523
+ }
1524
+ const commentText = sourceText.slice(range.pos, range.end);
1525
+ if (!commentText.startsWith("/**")) {
1526
+ continue;
1527
+ }
1528
+ parsedTags.push(...(0, import_internal2.parseCommentBlock)(commentText, { offset: range.pos }).tags);
1529
+ }
1530
+ return parsedTags;
1531
+ }
1532
+ function findDiscriminatorProperty(node, fieldName) {
1533
+ if (ts3.isClassDeclaration(node)) {
1534
+ for (const member of node.members) {
1535
+ if (ts3.isPropertyDeclaration(member) && ts3.isIdentifier(member.name) && member.name.text === fieldName) {
1536
+ return member;
1537
+ }
1538
+ }
1539
+ return null;
1540
+ }
1541
+ if (ts3.isInterfaceDeclaration(node)) {
1542
+ for (const member of node.members) {
1543
+ if (ts3.isPropertySignature(member) && ts3.isIdentifier(member.name) && member.name.text === fieldName) {
1544
+ return member;
1545
+ }
1546
+ }
1547
+ return null;
1548
+ }
1549
+ if (ts3.isTypeLiteralNode(node.type)) {
1550
+ for (const member of node.type.members) {
1551
+ if (ts3.isPropertySignature(member) && ts3.isIdentifier(member.name) && member.name.text === fieldName) {
1552
+ return member;
1553
+ }
1554
+ }
1555
+ }
1556
+ return null;
1557
+ }
1558
+ function isLocalTypeParameterName(node, typeParameterName) {
1559
+ return node.typeParameters?.some((typeParameter) => typeParameter.name.text === typeParameterName) ?? false;
1560
+ }
1561
+ function isNullishSemanticType(type) {
1562
+ if (type.flags & (ts3.TypeFlags.Null | ts3.TypeFlags.Undefined | ts3.TypeFlags.Void | ts3.TypeFlags.Unknown | ts3.TypeFlags.Any)) {
1563
+ return true;
1564
+ }
1565
+ return type.isUnion() && type.types.some((member) => isNullishSemanticType(member));
1566
+ }
1567
+ function isStringLikeSemanticType(type) {
1568
+ if (type.flags & ts3.TypeFlags.StringLike) {
1569
+ return true;
1570
+ }
1571
+ if (type.isUnion()) {
1572
+ return type.types.length > 0 && type.types.every((member) => isStringLikeSemanticType(member));
1573
+ }
1574
+ return false;
1575
+ }
1576
+ function extractDiscriminatorDirective(node, file, diagnostics) {
1577
+ const discriminatorTags = getLeadingParsedTags(node).filter(
1578
+ (tag) => tag.normalizedTagName === "discriminator"
1579
+ );
1580
+ if (discriminatorTags.length === 0) {
1581
+ return null;
1582
+ }
1583
+ const [firstTag, ...duplicateTags] = discriminatorTags;
1584
+ for (const _duplicateTag of duplicateTags) {
1585
+ diagnostics.push(
1586
+ makeAnalysisDiagnostic(
1587
+ "DUPLICATE_TAG",
1588
+ 'Duplicate "@discriminator" tag. Only one discriminator declaration is allowed per declaration.',
1589
+ provenanceForNode(node, file)
1590
+ )
1591
+ );
1592
+ }
1593
+ if (firstTag === void 0) {
1594
+ return null;
1595
+ }
1596
+ const firstTarget = firstTag.target;
1597
+ if (firstTarget?.path === null || firstTarget?.valid !== true) {
1598
+ diagnostics.push(
1599
+ makeAnalysisDiagnostic(
1600
+ "INVALID_TAG_ARGUMENT",
1601
+ 'Tag "@discriminator" requires a direct path target like ":kind".',
1602
+ provenanceForNode(node, file)
1603
+ )
1604
+ );
1605
+ return null;
1606
+ }
1607
+ if (firstTarget.path.segments.length !== 1) {
1608
+ diagnostics.push(
1609
+ makeAnalysisDiagnostic(
1610
+ "INVALID_TAG_ARGUMENT",
1611
+ 'Tag "@discriminator" only supports direct property targets in v1; nested paths are out of scope.',
1612
+ provenanceForNode(node, file)
1613
+ )
1614
+ );
1615
+ return null;
1616
+ }
1617
+ const typeParameterName = firstTag.argumentText.trim();
1618
+ if (!/^[A-Za-z_$][\w$]*$/u.test(typeParameterName)) {
1619
+ diagnostics.push(
1620
+ makeAnalysisDiagnostic(
1621
+ "INVALID_TAG_ARGUMENT",
1622
+ 'Tag "@discriminator" requires a local type parameter name as its source operand.',
1623
+ provenanceForNode(node, file)
1624
+ )
1625
+ );
1626
+ return null;
1627
+ }
1628
+ return {
1629
+ fieldName: firstTarget.path.segments[0] ?? firstTarget.rawText,
1630
+ typeParameterName,
1631
+ provenance: provenanceForNode(node, file)
1632
+ };
1633
+ }
1634
+ function validateDiscriminatorDirective(node, checker, file, diagnostics) {
1635
+ const directive = extractDiscriminatorDirective(node, file, diagnostics);
1636
+ if (directive === null) {
1637
+ return null;
1638
+ }
1639
+ if (!isLocalTypeParameterName(node, directive.typeParameterName)) {
1640
+ diagnostics.push(
1641
+ makeAnalysisDiagnostic(
1642
+ "INVALID_TAG_ARGUMENT",
1643
+ `Tag "@discriminator" references "${directive.typeParameterName}", but the source operand must be a type parameter declared on the same declaration.`,
1644
+ directive.provenance
1645
+ )
1646
+ );
1647
+ return null;
1648
+ }
1649
+ const propertyDecl = findDiscriminatorProperty(node, directive.fieldName);
1650
+ if (propertyDecl === null) {
1651
+ diagnostics.push(
1652
+ makeAnalysisDiagnostic(
1653
+ "UNKNOWN_PATH_TARGET",
1654
+ `Tag "@discriminator" targets "${directive.fieldName}", but no direct property with that name exists on this declaration.`,
1655
+ directive.provenance
1656
+ )
1657
+ );
1658
+ return null;
1659
+ }
1660
+ if (propertyDecl.questionToken !== void 0) {
1661
+ diagnostics.push(
1662
+ makeAnalysisDiagnostic(
1663
+ "TYPE_MISMATCH",
1664
+ `Discriminator field "${directive.fieldName}" must be required; optional discriminator fields are not supported.`,
1665
+ directive.provenance,
1666
+ [provenanceForNode(propertyDecl, file)]
1667
+ )
1668
+ );
1669
+ return null;
1670
+ }
1671
+ const propertyType = checker.getTypeAtLocation(propertyDecl);
1672
+ if (isNullishSemanticType(propertyType)) {
1673
+ diagnostics.push(
1674
+ makeAnalysisDiagnostic(
1675
+ "TYPE_MISMATCH",
1676
+ `Discriminator field "${directive.fieldName}" must not be nullable.`,
1677
+ directive.provenance,
1678
+ [provenanceForNode(propertyDecl, file)]
1679
+ )
1680
+ );
1681
+ return null;
1682
+ }
1683
+ if (!isStringLikeSemanticType(propertyType)) {
1684
+ diagnostics.push(
1685
+ makeAnalysisDiagnostic(
1686
+ "TYPE_MISMATCH",
1687
+ `Discriminator field "${directive.fieldName}" must be string-like.`,
1688
+ directive.provenance,
1689
+ [provenanceForNode(propertyDecl, file)]
1690
+ )
1691
+ );
1692
+ return null;
1693
+ }
1694
+ return directive;
1695
+ }
1696
+ function getConcreteTypeArgumentForDiscriminator(node, subjectType, checker, typeParameterName) {
1697
+ const typeParameterIndex = node.typeParameters?.findIndex(
1698
+ (typeParameter) => typeParameter.name.text === typeParameterName
1699
+ ) ?? -1;
1700
+ if (typeParameterIndex < 0) {
1701
+ return null;
1702
+ }
1703
+ const referenceTypeArguments = (isTypeReference(subjectType) ? subjectType.typeArguments : void 0) ?? subjectType.aliasTypeArguments;
1704
+ if (referenceTypeArguments?.[typeParameterIndex] !== void 0) {
1705
+ return referenceTypeArguments[typeParameterIndex] ?? null;
1706
+ }
1707
+ const localTypeParameter = node.typeParameters?.[typeParameterIndex];
1708
+ return localTypeParameter === void 0 ? null : checker.getTypeAtLocation(localTypeParameter);
1709
+ }
1710
+ function extractDeclarationApiName(node) {
1711
+ for (const tag of getLeadingParsedTags(node)) {
1712
+ if (tag.normalizedTagName !== "apiName") {
1713
+ continue;
1714
+ }
1715
+ if (tag.target === null && tag.argumentText.trim() !== "") {
1716
+ return tag.argumentText.trim();
1717
+ }
1718
+ if (tag.target?.kind === "variant" && tag.target.rawText === "singular") {
1719
+ const value = tag.argumentText.trim();
1720
+ if (value !== "") {
1721
+ return value;
1722
+ }
1723
+ }
1724
+ }
1725
+ return null;
1726
+ }
1727
+ function inferJsonFacingName(name) {
1728
+ return name.replace(/([a-z0-9])([A-Z])/g, "$1_$2").replace(/([A-Z]+)([A-Z][a-z])/g, "$1_$2").replace(/[-\s]+/g, "_").toLowerCase();
1729
+ }
1730
+ function resolveNamedDiscriminatorDeclaration(type, checker, seen = /* @__PURE__ */ new Set()) {
1731
+ if (seen.has(type)) {
1732
+ return null;
1733
+ }
1734
+ seen.add(type);
1735
+ const symbol = type.aliasSymbol ?? type.getSymbol();
1736
+ if (symbol !== void 0) {
1737
+ const aliased = symbol.flags & ts3.SymbolFlags.Alias ? checker.getAliasedSymbol(symbol) : void 0;
1738
+ const targetSymbol = aliased ?? symbol;
1739
+ const declaration = targetSymbol.declarations?.find(
1740
+ (candidate) => ts3.isClassDeclaration(candidate) || ts3.isInterfaceDeclaration(candidate) || ts3.isTypeAliasDeclaration(candidate) || ts3.isEnumDeclaration(candidate)
1741
+ );
1742
+ if (declaration !== void 0) {
1743
+ if (ts3.isTypeAliasDeclaration(declaration) && ts3.isTypeReferenceNode(declaration.type) && checker.getTypeFromTypeNode(declaration.type) !== type) {
1744
+ return resolveNamedDiscriminatorDeclaration(
1745
+ checker.getTypeFromTypeNode(declaration.type),
1746
+ checker,
1747
+ seen
1748
+ );
1749
+ }
1750
+ return declaration;
1751
+ }
1752
+ }
1753
+ return null;
1754
+ }
1755
+ function resolveDiscriminatorValue(boundType, checker, provenance, diagnostics) {
1756
+ if (boundType === null) {
1757
+ diagnostics.push(
1758
+ makeAnalysisDiagnostic(
1759
+ "INVALID_TAG_ARGUMENT",
1760
+ "Discriminator resolution failed because no concrete type argument is available for the referenced type parameter.",
1761
+ provenance
1762
+ )
1763
+ );
1764
+ return null;
1765
+ }
1766
+ if (boundType.isStringLiteral()) {
1767
+ return boundType.value;
1768
+ }
1769
+ if (boundType.isUnion()) {
1770
+ const nonNullMembers = boundType.types.filter(
1771
+ (member) => !(member.flags & (ts3.TypeFlags.Null | ts3.TypeFlags.Undefined))
1772
+ );
1773
+ if (nonNullMembers.every((member) => member.isStringLiteral())) {
1774
+ diagnostics.push(
1775
+ makeAnalysisDiagnostic(
1776
+ "INVALID_TAG_ARGUMENT",
1777
+ "Discriminator resolution for unions of string literals is out of scope for v1.",
1778
+ provenance
1779
+ )
1780
+ );
1781
+ return null;
1782
+ }
1783
+ }
1784
+ const declaration = resolveNamedDiscriminatorDeclaration(boundType, checker);
1785
+ if (declaration !== null) {
1786
+ return extractDeclarationApiName(declaration) ?? inferJsonFacingName(getDeclarationName(declaration));
1787
+ }
1788
+ diagnostics.push(
1789
+ makeAnalysisDiagnostic(
1790
+ "INVALID_TAG_ARGUMENT",
1791
+ "Discriminator resolution could not derive a JSON-facing discriminator value from the referenced type argument.",
1792
+ provenance
1793
+ )
1794
+ );
1795
+ return null;
1796
+ }
1797
+ function getDeclarationName(node) {
1798
+ if (ts3.isClassDeclaration(node) || ts3.isInterfaceDeclaration(node) || ts3.isTypeAliasDeclaration(node) || ts3.isEnumDeclaration(node)) {
1799
+ return node.name?.text ?? "anonymous";
1800
+ }
1801
+ return "anonymous";
1802
+ }
1803
+ function applyDeclarationDiscriminatorToFields(fields, node, subjectType, checker, file, diagnostics) {
1804
+ const directive = validateDiscriminatorDirective(node, checker, file, diagnostics);
1805
+ if (directive === null) {
1806
+ return [...fields];
1807
+ }
1808
+ const discriminatorValue = resolveDiscriminatorValue(
1809
+ getConcreteTypeArgumentForDiscriminator(
1810
+ node,
1811
+ subjectType,
1812
+ checker,
1813
+ directive.typeParameterName
1814
+ ),
1815
+ checker,
1816
+ directive.provenance,
1817
+ diagnostics
1818
+ );
1819
+ if (discriminatorValue === null) {
1820
+ return [...fields];
1821
+ }
1822
+ return fields.map(
1823
+ (field) => field.name === directive.fieldName ? {
1824
+ ...field,
1825
+ type: {
1826
+ kind: "enum",
1827
+ members: [{ value: discriminatorValue }]
1828
+ }
1829
+ } : field
1830
+ );
1831
+ }
1832
+ function buildInstantiatedReferenceName(baseName, typeArguments, checker) {
1833
+ const renderedArguments = typeArguments.map(
1834
+ (typeArgument) => checker.typeToString(typeArgument).replace(/[^A-Za-z0-9]+/g, "_").replace(/^_+|_+$/g, "")
1835
+ ).filter((value) => value !== "");
1836
+ return renderedArguments.length === 0 ? baseName : `${baseName}__${renderedArguments.join("__")}`;
1837
+ }
1838
+ function extractReferenceTypeArguments(type, checker, file, typeRegistry, visiting, sourceNode, extensionRegistry, diagnostics) {
1839
+ const typeNode = sourceNode === void 0 ? void 0 : extractTypeNodeFromSource(sourceNode);
1840
+ if (typeNode === void 0) {
1841
+ return [];
1842
+ }
1843
+ const resolvedTypeNode = resolveAliasedTypeNode(typeNode, checker);
1844
+ if (!ts3.isTypeReferenceNode(resolvedTypeNode) || resolvedTypeNode.typeArguments === void 0) {
1845
+ return [];
1846
+ }
1847
+ return resolvedTypeNode.typeArguments.map((argumentNode) => {
1848
+ const argumentType = checker.getTypeFromTypeNode(argumentNode);
1849
+ return {
1850
+ tsType: argumentType,
1851
+ typeNode: resolveTypeNode(
1852
+ argumentType,
1853
+ checker,
1854
+ file,
1855
+ typeRegistry,
1856
+ visiting,
1857
+ argumentNode,
1858
+ extensionRegistry,
1859
+ diagnostics
1860
+ )
1861
+ };
1862
+ });
1863
+ }
1864
+ function applyDiscriminatorToObjectProperties(properties, node, subjectType, checker, file, diagnostics) {
1865
+ const directive = validateDiscriminatorDirective(node, checker, file, diagnostics);
1866
+ if (directive === null) {
1867
+ return properties;
1868
+ }
1869
+ const discriminatorValue = resolveDiscriminatorValue(
1870
+ getConcreteTypeArgumentForDiscriminator(
1871
+ node,
1872
+ subjectType,
1873
+ checker,
1874
+ directive.typeParameterName
1875
+ ),
1876
+ checker,
1877
+ directive.provenance,
1878
+ diagnostics
1879
+ );
1880
+ if (discriminatorValue === null) {
1881
+ return properties;
1882
+ }
1883
+ return properties.map(
1884
+ (property) => property.name === directive.fieldName ? {
1885
+ ...property,
1886
+ type: {
1887
+ kind: "enum",
1888
+ members: [{ value: discriminatorValue }]
1889
+ }
1890
+ } : property
1891
+ );
1892
+ }
1478
1893
  function analyzeFieldToIR(prop, checker, file, typeRegistry, visiting, diagnostics, hostType, extensionRegistry) {
1479
1894
  if (!ts3.isIdentifier(prop.name)) {
1480
1895
  return null;
@@ -1763,6 +2178,7 @@ function resolveTypeNode(type, checker, file, typeRegistry, visiting, sourceNode
1763
2178
  file,
1764
2179
  typeRegistry,
1765
2180
  visiting,
2181
+ sourceNode,
1766
2182
  extensionRegistry,
1767
2183
  diagnostics
1768
2184
  );
@@ -2026,35 +2442,60 @@ function typeNodeContainsReference(type, targetName) {
2026
2442
  }
2027
2443
  }
2028
2444
  }
2029
- function resolveObjectType(type, checker, file, typeRegistry, visiting, extensionRegistry, diagnostics) {
2445
+ function resolveObjectType(type, checker, file, typeRegistry, visiting, sourceNode, extensionRegistry, diagnostics) {
2446
+ const collectedDiagnostics = diagnostics ?? [];
2030
2447
  const typeName = getNamedTypeName(type);
2031
2448
  const namedTypeName = typeName ?? void 0;
2032
2449
  const namedDecl = getNamedTypeDeclaration(type);
2033
- const shouldRegisterNamedType = namedTypeName !== void 0 && !(namedTypeName === "Record" && namedDecl?.getSourceFile().fileName !== file);
2450
+ const referenceTypeArguments = extractReferenceTypeArguments(
2451
+ type,
2452
+ checker,
2453
+ file,
2454
+ typeRegistry,
2455
+ visiting,
2456
+ sourceNode,
2457
+ extensionRegistry,
2458
+ collectedDiagnostics
2459
+ );
2460
+ const instantiatedTypeName = namedTypeName !== void 0 && referenceTypeArguments.length > 0 ? buildInstantiatedReferenceName(
2461
+ namedTypeName,
2462
+ referenceTypeArguments.map((argument) => argument.tsType),
2463
+ checker
2464
+ ) : void 0;
2465
+ const registryTypeName = instantiatedTypeName ?? namedTypeName;
2466
+ const shouldRegisterNamedType = registryTypeName !== void 0 && !(registryTypeName === "Record" && namedDecl?.getSourceFile().fileName !== file);
2034
2467
  const clearNamedTypeRegistration = () => {
2035
- if (namedTypeName === void 0 || !shouldRegisterNamedType) {
2468
+ if (registryTypeName === void 0 || !shouldRegisterNamedType) {
2036
2469
  return;
2037
2470
  }
2038
- Reflect.deleteProperty(typeRegistry, namedTypeName);
2471
+ Reflect.deleteProperty(typeRegistry, registryTypeName);
2039
2472
  };
2040
2473
  if (visiting.has(type)) {
2041
- if (namedTypeName !== void 0 && shouldRegisterNamedType) {
2042
- return { kind: "reference", name: namedTypeName, typeArguments: [] };
2474
+ if (registryTypeName !== void 0 && shouldRegisterNamedType) {
2475
+ return {
2476
+ kind: "reference",
2477
+ name: registryTypeName,
2478
+ typeArguments: referenceTypeArguments.map((argument) => argument.typeNode)
2479
+ };
2043
2480
  }
2044
2481
  return { kind: "object", properties: [], additionalProperties: false };
2045
2482
  }
2046
- if (namedTypeName !== void 0 && shouldRegisterNamedType && !typeRegistry[namedTypeName]) {
2047
- typeRegistry[namedTypeName] = {
2048
- name: namedTypeName,
2483
+ if (registryTypeName !== void 0 && shouldRegisterNamedType && !typeRegistry[registryTypeName]) {
2484
+ typeRegistry[registryTypeName] = {
2485
+ name: registryTypeName,
2049
2486
  type: RESOLVING_TYPE_PLACEHOLDER,
2050
2487
  provenance: provenanceForDeclaration(namedDecl, file)
2051
2488
  };
2052
2489
  }
2053
2490
  visiting.add(type);
2054
- if (namedTypeName !== void 0 && shouldRegisterNamedType && typeRegistry[namedTypeName]?.type !== void 0) {
2055
- if (typeRegistry[namedTypeName].type !== RESOLVING_TYPE_PLACEHOLDER) {
2491
+ if (registryTypeName !== void 0 && shouldRegisterNamedType && typeRegistry[registryTypeName]?.type !== void 0) {
2492
+ if (typeRegistry[registryTypeName].type !== RESOLVING_TYPE_PLACEHOLDER) {
2056
2493
  visiting.delete(type);
2057
- return { kind: "reference", name: namedTypeName, typeArguments: [] };
2494
+ return {
2495
+ kind: "reference",
2496
+ name: registryTypeName,
2497
+ typeArguments: referenceTypeArguments.map((argument) => argument.typeNode)
2498
+ };
2058
2499
  }
2059
2500
  }
2060
2501
  const recordNode = tryResolveRecordType(
@@ -2064,24 +2505,28 @@ function resolveObjectType(type, checker, file, typeRegistry, visiting, extensio
2064
2505
  typeRegistry,
2065
2506
  visiting,
2066
2507
  extensionRegistry,
2067
- diagnostics
2508
+ collectedDiagnostics
2068
2509
  );
2069
2510
  if (recordNode) {
2070
2511
  visiting.delete(type);
2071
- if (namedTypeName !== void 0 && shouldRegisterNamedType) {
2072
- const isRecursiveRecord = typeNodeContainsReference(recordNode.valueType, namedTypeName);
2512
+ if (registryTypeName !== void 0 && shouldRegisterNamedType) {
2513
+ const isRecursiveRecord = typeNodeContainsReference(recordNode.valueType, registryTypeName);
2073
2514
  if (!isRecursiveRecord) {
2074
2515
  clearNamedTypeRegistration();
2075
2516
  return recordNode;
2076
2517
  }
2077
2518
  const annotations = namedDecl ? extractJSDocAnnotationNodes(namedDecl, file, makeParseOptions(extensionRegistry)) : void 0;
2078
- typeRegistry[namedTypeName] = {
2079
- name: namedTypeName,
2519
+ typeRegistry[registryTypeName] = {
2520
+ name: registryTypeName,
2080
2521
  type: recordNode,
2081
2522
  ...annotations !== void 0 && annotations.length > 0 && { annotations },
2082
2523
  provenance: provenanceForDeclaration(namedDecl, file)
2083
2524
  };
2084
- return { kind: "reference", name: namedTypeName, typeArguments: [] };
2525
+ return {
2526
+ kind: "reference",
2527
+ name: registryTypeName,
2528
+ typeArguments: referenceTypeArguments.map((argument) => argument.typeNode)
2529
+ };
2085
2530
  }
2086
2531
  return recordNode;
2087
2532
  }
@@ -2092,7 +2537,7 @@ function resolveObjectType(type, checker, file, typeRegistry, visiting, extensio
2092
2537
  file,
2093
2538
  typeRegistry,
2094
2539
  visiting,
2095
- diagnostics ?? [],
2540
+ collectedDiagnostics,
2096
2541
  extensionRegistry
2097
2542
  );
2098
2543
  for (const prop of type.getProperties()) {
@@ -2108,7 +2553,7 @@ function resolveObjectType(type, checker, file, typeRegistry, visiting, extensio
2108
2553
  visiting,
2109
2554
  declaration,
2110
2555
  extensionRegistry,
2111
- diagnostics
2556
+ collectedDiagnostics
2112
2557
  );
2113
2558
  const fieldNodeInfo = fieldInfoMap?.get(prop.name);
2114
2559
  properties.push({
@@ -2123,18 +2568,29 @@ function resolveObjectType(type, checker, file, typeRegistry, visiting, extensio
2123
2568
  visiting.delete(type);
2124
2569
  const objectNode = {
2125
2570
  kind: "object",
2126
- properties,
2571
+ properties: namedDecl !== void 0 && (ts3.isClassDeclaration(namedDecl) || ts3.isInterfaceDeclaration(namedDecl) || ts3.isTypeAliasDeclaration(namedDecl)) ? applyDiscriminatorToObjectProperties(
2572
+ properties,
2573
+ namedDecl,
2574
+ type,
2575
+ checker,
2576
+ file,
2577
+ collectedDiagnostics
2578
+ ) : properties,
2127
2579
  additionalProperties: true
2128
2580
  };
2129
- if (namedTypeName !== void 0 && shouldRegisterNamedType) {
2581
+ if (registryTypeName !== void 0 && shouldRegisterNamedType) {
2130
2582
  const annotations = namedDecl ? extractJSDocAnnotationNodes(namedDecl, file, makeParseOptions(extensionRegistry)) : void 0;
2131
- typeRegistry[namedTypeName] = {
2132
- name: namedTypeName,
2583
+ typeRegistry[registryTypeName] = {
2584
+ name: registryTypeName,
2133
2585
  type: objectNode,
2134
2586
  ...annotations !== void 0 && annotations.length > 0 && { annotations },
2135
2587
  provenance: provenanceForDeclaration(namedDecl, file)
2136
2588
  };
2137
- return { kind: "reference", name: namedTypeName, typeArguments: [] };
2589
+ return {
2590
+ kind: "reference",
2591
+ name: registryTypeName,
2592
+ typeArguments: referenceTypeArguments.map((argument) => argument.typeNode)
2593
+ };
2138
2594
  }
2139
2595
  return objectNode;
2140
2596
  }
@@ -3165,9 +3621,9 @@ function generateUiSchemaFromIR(ir) {
3165
3621
  }
3166
3622
 
3167
3623
  // src/validate/constraint-validator.ts
3168
- var import_internal2 = require("@formspec/analysis/internal");
3624
+ var import_internal3 = require("@formspec/analysis/internal");
3169
3625
  function validateFieldNode(ctx, field) {
3170
- const analysis = (0, import_internal2.analyzeConstraintTargets)(
3626
+ const analysis = (0, import_internal3.analyzeConstraintTargets)(
3171
3627
  field.name,
3172
3628
  field.type,
3173
3629
  field.constraints,
@@ -3185,7 +3641,7 @@ function validateFieldNode(ctx, field) {
3185
3641
  }
3186
3642
  function validateObjectProperty(ctx, parentName, property) {
3187
3643
  const qualifiedName = `${parentName}.${property.name}`;
3188
- const analysis = (0, import_internal2.analyzeConstraintTargets)(
3644
+ const analysis = (0, import_internal3.analyzeConstraintTargets)(
3189
3645
  qualifiedName,
3190
3646
  property.type,
3191
3647
  property.constraints,
@@ -3358,7 +3814,23 @@ var import_internals5 = require("@formspec/core/internals");
3358
3814
  function typeToJsonSchema(type, checker) {
3359
3815
  const typeRegistry = {};
3360
3816
  const visiting = /* @__PURE__ */ new Set();
3361
- const typeNode = resolveTypeNode(type, checker, "", typeRegistry, visiting);
3817
+ const diagnostics = [];
3818
+ const typeNode = resolveTypeNode(
3819
+ type,
3820
+ checker,
3821
+ "",
3822
+ typeRegistry,
3823
+ visiting,
3824
+ void 0,
3825
+ void 0,
3826
+ diagnostics
3827
+ );
3828
+ if (diagnostics.length > 0) {
3829
+ const diagnosticDetails = diagnostics.map((diagnostic) => `${diagnostic.code}: ${diagnostic.message}`).join("; ");
3830
+ throw new Error(
3831
+ `FormSpec validation failed while resolving method schema types. ${diagnosticDetails}`
3832
+ );
3833
+ }
3362
3834
  const fieldProvenance = { surface: "tsdoc", file: "", line: 0, column: 0 };
3363
3835
  const ir = {
3364
3836
  kind: "form-ir",