@formspec/build 0.1.0-alpha.26 → 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.
@@ -32,12 +32,14 @@ var internals_exports = {};
32
32
  __export(internals_exports, {
33
33
  analyzeClassToIR: () => analyzeClassToIR,
34
34
  analyzeInterfaceToIR: () => analyzeInterfaceToIR,
35
+ analyzeNamedTypeToIRFromProgramContext: () => analyzeNamedTypeToIRFromProgramContext,
35
36
  analyzeTypeAliasToIR: () => analyzeTypeAliasToIR,
36
37
  canonicalizeChainDSL: () => canonicalizeChainDSL,
37
38
  canonicalizeTSDoc: () => canonicalizeTSDoc,
38
39
  collectFormSpecReferences: () => collectFormSpecReferences,
39
40
  createExtensionRegistry: () => createExtensionRegistry,
40
41
  createProgramContext: () => createProgramContext,
42
+ createProgramContextFromProgram: () => createProgramContextFromProgram,
41
43
  findClassByName: () => findClassByName,
42
44
  findInterfaceByName: () => findInterfaceByName,
43
45
  findTypeAliasByName: () => findTypeAliasByName,
@@ -443,6 +445,7 @@ var path = __toESM(require("path"), 1);
443
445
 
444
446
  // src/analyzer/class-analyzer.ts
445
447
  var ts3 = __toESM(require("typescript"), 1);
448
+ var import_internal2 = require("@formspec/analysis/internal");
446
449
 
447
450
  // src/analyzer/jsdoc-constraints.ts
448
451
  var ts2 = __toESM(require("typescript"), 1);
@@ -1365,9 +1368,17 @@ function analyzeClassToIR(classDecl, checker, file = "", extensionRegistry) {
1365
1368
  }
1366
1369
  }
1367
1370
  }
1371
+ const specializedFields = applyDeclarationDiscriminatorToFields(
1372
+ fields,
1373
+ classDecl,
1374
+ classType,
1375
+ checker,
1376
+ file,
1377
+ diagnostics
1378
+ );
1368
1379
  return {
1369
1380
  name,
1370
- fields,
1381
+ fields: specializedFields,
1371
1382
  fieldLayouts,
1372
1383
  typeRegistry,
1373
1384
  ...annotations.length > 0 && { annotations },
@@ -1407,10 +1418,18 @@ function analyzeInterfaceToIR(interfaceDecl, checker, file = "", extensionRegist
1407
1418
  }
1408
1419
  }
1409
1420
  }
1410
- 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(() => ({}));
1411
1430
  return {
1412
1431
  name,
1413
- fields,
1432
+ fields: specializedFields,
1414
1433
  fieldLayouts,
1415
1434
  typeRegistry,
1416
1435
  ...annotations.length > 0 && { annotations },
@@ -1459,12 +1478,20 @@ function analyzeTypeAliasToIR(typeAlias, checker, file = "", extensionRegistry)
1459
1478
  }
1460
1479
  }
1461
1480
  }
1481
+ const specializedFields = applyDeclarationDiscriminatorToFields(
1482
+ fields,
1483
+ typeAlias,
1484
+ aliasType,
1485
+ checker,
1486
+ file,
1487
+ diagnostics
1488
+ );
1462
1489
  return {
1463
1490
  ok: true,
1464
1491
  analysis: {
1465
1492
  name,
1466
- fields,
1467
- fieldLayouts: fields.map(() => ({})),
1493
+ fields: specializedFields,
1494
+ fieldLayouts: specializedFields.map(() => ({})),
1468
1495
  typeRegistry,
1469
1496
  ...annotations.length > 0 && { annotations },
1470
1497
  ...diagnostics.length > 0 && { diagnostics },
@@ -1473,6 +1500,396 @@ function analyzeTypeAliasToIR(typeAlias, checker, file = "", extensionRegistry)
1473
1500
  }
1474
1501
  };
1475
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
+ }
1476
1893
  function analyzeFieldToIR(prop, checker, file, typeRegistry, visiting, diagnostics, hostType, extensionRegistry) {
1477
1894
  if (!ts3.isIdentifier(prop.name)) {
1478
1895
  return null;
@@ -1761,6 +2178,7 @@ function resolveTypeNode(type, checker, file, typeRegistry, visiting, sourceNode
1761
2178
  file,
1762
2179
  typeRegistry,
1763
2180
  visiting,
2181
+ sourceNode,
1764
2182
  extensionRegistry,
1765
2183
  diagnostics
1766
2184
  );
@@ -2024,35 +2442,60 @@ function typeNodeContainsReference(type, targetName) {
2024
2442
  }
2025
2443
  }
2026
2444
  }
2027
- function resolveObjectType(type, checker, file, typeRegistry, visiting, extensionRegistry, diagnostics) {
2445
+ function resolveObjectType(type, checker, file, typeRegistry, visiting, sourceNode, extensionRegistry, diagnostics) {
2446
+ const collectedDiagnostics = diagnostics ?? [];
2028
2447
  const typeName = getNamedTypeName(type);
2029
2448
  const namedTypeName = typeName ?? void 0;
2030
2449
  const namedDecl = getNamedTypeDeclaration(type);
2031
- 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);
2032
2467
  const clearNamedTypeRegistration = () => {
2033
- if (namedTypeName === void 0 || !shouldRegisterNamedType) {
2468
+ if (registryTypeName === void 0 || !shouldRegisterNamedType) {
2034
2469
  return;
2035
2470
  }
2036
- Reflect.deleteProperty(typeRegistry, namedTypeName);
2471
+ Reflect.deleteProperty(typeRegistry, registryTypeName);
2037
2472
  };
2038
2473
  if (visiting.has(type)) {
2039
- if (namedTypeName !== void 0 && shouldRegisterNamedType) {
2040
- 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
+ };
2041
2480
  }
2042
2481
  return { kind: "object", properties: [], additionalProperties: false };
2043
2482
  }
2044
- if (namedTypeName !== void 0 && shouldRegisterNamedType && !typeRegistry[namedTypeName]) {
2045
- typeRegistry[namedTypeName] = {
2046
- name: namedTypeName,
2483
+ if (registryTypeName !== void 0 && shouldRegisterNamedType && !typeRegistry[registryTypeName]) {
2484
+ typeRegistry[registryTypeName] = {
2485
+ name: registryTypeName,
2047
2486
  type: RESOLVING_TYPE_PLACEHOLDER,
2048
2487
  provenance: provenanceForDeclaration(namedDecl, file)
2049
2488
  };
2050
2489
  }
2051
2490
  visiting.add(type);
2052
- if (namedTypeName !== void 0 && shouldRegisterNamedType && typeRegistry[namedTypeName]?.type !== void 0) {
2053
- 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) {
2054
2493
  visiting.delete(type);
2055
- return { kind: "reference", name: namedTypeName, typeArguments: [] };
2494
+ return {
2495
+ kind: "reference",
2496
+ name: registryTypeName,
2497
+ typeArguments: referenceTypeArguments.map((argument) => argument.typeNode)
2498
+ };
2056
2499
  }
2057
2500
  }
2058
2501
  const recordNode = tryResolveRecordType(
@@ -2062,24 +2505,28 @@ function resolveObjectType(type, checker, file, typeRegistry, visiting, extensio
2062
2505
  typeRegistry,
2063
2506
  visiting,
2064
2507
  extensionRegistry,
2065
- diagnostics
2508
+ collectedDiagnostics
2066
2509
  );
2067
2510
  if (recordNode) {
2068
2511
  visiting.delete(type);
2069
- if (namedTypeName !== void 0 && shouldRegisterNamedType) {
2070
- const isRecursiveRecord = typeNodeContainsReference(recordNode.valueType, namedTypeName);
2512
+ if (registryTypeName !== void 0 && shouldRegisterNamedType) {
2513
+ const isRecursiveRecord = typeNodeContainsReference(recordNode.valueType, registryTypeName);
2071
2514
  if (!isRecursiveRecord) {
2072
2515
  clearNamedTypeRegistration();
2073
2516
  return recordNode;
2074
2517
  }
2075
2518
  const annotations = namedDecl ? extractJSDocAnnotationNodes(namedDecl, file, makeParseOptions(extensionRegistry)) : void 0;
2076
- typeRegistry[namedTypeName] = {
2077
- name: namedTypeName,
2519
+ typeRegistry[registryTypeName] = {
2520
+ name: registryTypeName,
2078
2521
  type: recordNode,
2079
2522
  ...annotations !== void 0 && annotations.length > 0 && { annotations },
2080
2523
  provenance: provenanceForDeclaration(namedDecl, file)
2081
2524
  };
2082
- return { kind: "reference", name: namedTypeName, typeArguments: [] };
2525
+ return {
2526
+ kind: "reference",
2527
+ name: registryTypeName,
2528
+ typeArguments: referenceTypeArguments.map((argument) => argument.typeNode)
2529
+ };
2083
2530
  }
2084
2531
  return recordNode;
2085
2532
  }
@@ -2090,7 +2537,7 @@ function resolveObjectType(type, checker, file, typeRegistry, visiting, extensio
2090
2537
  file,
2091
2538
  typeRegistry,
2092
2539
  visiting,
2093
- diagnostics ?? [],
2540
+ collectedDiagnostics,
2094
2541
  extensionRegistry
2095
2542
  );
2096
2543
  for (const prop of type.getProperties()) {
@@ -2106,7 +2553,7 @@ function resolveObjectType(type, checker, file, typeRegistry, visiting, extensio
2106
2553
  visiting,
2107
2554
  declaration,
2108
2555
  extensionRegistry,
2109
- diagnostics
2556
+ collectedDiagnostics
2110
2557
  );
2111
2558
  const fieldNodeInfo = fieldInfoMap?.get(prop.name);
2112
2559
  properties.push({
@@ -2121,18 +2568,29 @@ function resolveObjectType(type, checker, file, typeRegistry, visiting, extensio
2121
2568
  visiting.delete(type);
2122
2569
  const objectNode = {
2123
2570
  kind: "object",
2124
- 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,
2125
2579
  additionalProperties: true
2126
2580
  };
2127
- if (namedTypeName !== void 0 && shouldRegisterNamedType) {
2581
+ if (registryTypeName !== void 0 && shouldRegisterNamedType) {
2128
2582
  const annotations = namedDecl ? extractJSDocAnnotationNodes(namedDecl, file, makeParseOptions(extensionRegistry)) : void 0;
2129
- typeRegistry[namedTypeName] = {
2130
- name: namedTypeName,
2583
+ typeRegistry[registryTypeName] = {
2584
+ name: registryTypeName,
2131
2585
  type: objectNode,
2132
2586
  ...annotations !== void 0 && annotations.length > 0 && { annotations },
2133
2587
  provenance: provenanceForDeclaration(namedDecl, file)
2134
2588
  };
2135
- return { kind: "reference", name: namedTypeName, typeArguments: [] };
2589
+ return {
2590
+ kind: "reference",
2591
+ name: registryTypeName,
2592
+ typeArguments: referenceTypeArguments.map((argument) => argument.typeNode)
2593
+ };
2136
2594
  }
2137
2595
  return objectNode;
2138
2596
  }
@@ -2393,6 +2851,18 @@ function detectFormSpecReference(typeNode) {
2393
2851
  }
2394
2852
 
2395
2853
  // src/analyzer/program.ts
2854
+ function createProgramContextFromProgram(program, filePath) {
2855
+ const absolutePath = path.resolve(filePath);
2856
+ const sourceFile = program.getSourceFile(absolutePath) ?? program.getSourceFile(filePath);
2857
+ if (!sourceFile) {
2858
+ throw new Error(`Could not find source file in provided program: ${absolutePath}`);
2859
+ }
2860
+ return {
2861
+ program,
2862
+ checker: program.getTypeChecker(),
2863
+ sourceFile
2864
+ };
2865
+ }
2396
2866
  function createProgramContext(filePath) {
2397
2867
  const absolutePath = path.resolve(filePath);
2398
2868
  const fileDir = path.dirname(absolutePath);
@@ -2461,6 +2931,36 @@ function findInterfaceByName(sourceFile, interfaceName) {
2461
2931
  function findTypeAliasByName(sourceFile, aliasName) {
2462
2932
  return findNodeByName(sourceFile, aliasName, ts4.isTypeAliasDeclaration, (n) => n.name.text);
2463
2933
  }
2934
+ function analyzeNamedTypeToIRFromProgramContext(ctx, filePath, typeName, extensionRegistry) {
2935
+ const analysisFilePath = path.resolve(filePath);
2936
+ const classDecl = findClassByName(ctx.sourceFile, typeName);
2937
+ if (classDecl !== null) {
2938
+ return analyzeClassToIR(classDecl, ctx.checker, analysisFilePath, extensionRegistry);
2939
+ }
2940
+ const interfaceDecl = findInterfaceByName(ctx.sourceFile, typeName);
2941
+ if (interfaceDecl !== null) {
2942
+ return analyzeInterfaceToIR(interfaceDecl, ctx.checker, analysisFilePath, extensionRegistry);
2943
+ }
2944
+ const typeAlias = findTypeAliasByName(ctx.sourceFile, typeName);
2945
+ if (typeAlias !== null) {
2946
+ const result = analyzeTypeAliasToIR(
2947
+ typeAlias,
2948
+ ctx.checker,
2949
+ analysisFilePath,
2950
+ extensionRegistry
2951
+ );
2952
+ if (result.ok) {
2953
+ return result.analysis;
2954
+ }
2955
+ throw new Error(result.error);
2956
+ }
2957
+ throw new Error(
2958
+ `Type "${typeName}" not found as a class, interface, or type alias in ${analysisFilePath}`
2959
+ );
2960
+ }
2961
+
2962
+ // src/generators/class-schema.ts
2963
+ var ts5 = require("typescript");
2464
2964
 
2465
2965
  // src/json-schema/ir-generator.ts
2466
2966
  function makeContext(options) {
@@ -3121,9 +3621,9 @@ function generateUiSchemaFromIR(ir) {
3121
3621
  }
3122
3622
 
3123
3623
  // src/validate/constraint-validator.ts
3124
- var import_internal2 = require("@formspec/analysis/internal");
3624
+ var import_internal3 = require("@formspec/analysis/internal");
3125
3625
  function validateFieldNode(ctx, field) {
3126
- const analysis = (0, import_internal2.analyzeConstraintTargets)(
3626
+ const analysis = (0, import_internal3.analyzeConstraintTargets)(
3127
3627
  field.name,
3128
3628
  field.type,
3129
3629
  field.constraints,
@@ -3141,7 +3641,7 @@ function validateFieldNode(ctx, field) {
3141
3641
  }
3142
3642
  function validateObjectProperty(ctx, parentName, property) {
3143
3643
  const qualifiedName = `${parentName}.${property.name}`;
3144
- const analysis = (0, import_internal2.analyzeConstraintTargets)(
3644
+ const analysis = (0, import_internal3.analyzeConstraintTargets)(
3145
3645
  qualifiedName,
3146
3646
  property.type,
3147
3647
  property.constraints,
@@ -3314,7 +3814,23 @@ var import_internals5 = require("@formspec/core/internals");
3314
3814
  function typeToJsonSchema(type, checker) {
3315
3815
  const typeRegistry = {};
3316
3816
  const visiting = /* @__PURE__ */ new Set();
3317
- 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
+ }
3318
3834
  const fieldProvenance = { surface: "tsdoc", file: "", line: 0, column: 0 };
3319
3835
  const ir = {
3320
3836
  kind: "form-ir",
@@ -3415,12 +3931,14 @@ function collectFormSpecReferences(methods) {
3415
3931
  0 && (module.exports = {
3416
3932
  analyzeClassToIR,
3417
3933
  analyzeInterfaceToIR,
3934
+ analyzeNamedTypeToIRFromProgramContext,
3418
3935
  analyzeTypeAliasToIR,
3419
3936
  canonicalizeChainDSL,
3420
3937
  canonicalizeTSDoc,
3421
3938
  collectFormSpecReferences,
3422
3939
  createExtensionRegistry,
3423
3940
  createProgramContext,
3941
+ createProgramContextFromProgram,
3424
3942
  findClassByName,
3425
3943
  findInterfaceByName,
3426
3944
  findTypeAliasByName,