@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.
package/dist/internals.js CHANGED
@@ -392,6 +392,9 @@ import * as path from "path";
392
392
 
393
393
  // src/analyzer/class-analyzer.ts
394
394
  import * as ts3 from "typescript";
395
+ import {
396
+ parseCommentBlock as parseCommentBlock2
397
+ } from "@formspec/analysis/internal";
395
398
 
396
399
  // src/analyzer/jsdoc-constraints.ts
397
400
  import * as ts2 from "typescript";
@@ -1339,9 +1342,17 @@ function analyzeClassToIR(classDecl, checker, file = "", extensionRegistry) {
1339
1342
  }
1340
1343
  }
1341
1344
  }
1345
+ const specializedFields = applyDeclarationDiscriminatorToFields(
1346
+ fields,
1347
+ classDecl,
1348
+ classType,
1349
+ checker,
1350
+ file,
1351
+ diagnostics
1352
+ );
1342
1353
  return {
1343
1354
  name,
1344
- fields,
1355
+ fields: specializedFields,
1345
1356
  fieldLayouts,
1346
1357
  typeRegistry,
1347
1358
  ...annotations.length > 0 && { annotations },
@@ -1381,10 +1392,18 @@ function analyzeInterfaceToIR(interfaceDecl, checker, file = "", extensionRegist
1381
1392
  }
1382
1393
  }
1383
1394
  }
1384
- const fieldLayouts = fields.map(() => ({}));
1395
+ const specializedFields = applyDeclarationDiscriminatorToFields(
1396
+ fields,
1397
+ interfaceDecl,
1398
+ interfaceType,
1399
+ checker,
1400
+ file,
1401
+ diagnostics
1402
+ );
1403
+ const fieldLayouts = specializedFields.map(() => ({}));
1385
1404
  return {
1386
1405
  name,
1387
- fields,
1406
+ fields: specializedFields,
1388
1407
  fieldLayouts,
1389
1408
  typeRegistry,
1390
1409
  ...annotations.length > 0 && { annotations },
@@ -1433,12 +1452,20 @@ function analyzeTypeAliasToIR(typeAlias, checker, file = "", extensionRegistry)
1433
1452
  }
1434
1453
  }
1435
1454
  }
1455
+ const specializedFields = applyDeclarationDiscriminatorToFields(
1456
+ fields,
1457
+ typeAlias,
1458
+ aliasType,
1459
+ checker,
1460
+ file,
1461
+ diagnostics
1462
+ );
1436
1463
  return {
1437
1464
  ok: true,
1438
1465
  analysis: {
1439
1466
  name,
1440
- fields,
1441
- fieldLayouts: fields.map(() => ({})),
1467
+ fields: specializedFields,
1468
+ fieldLayouts: specializedFields.map(() => ({})),
1442
1469
  typeRegistry,
1443
1470
  ...annotations.length > 0 && { annotations },
1444
1471
  ...diagnostics.length > 0 && { diagnostics },
@@ -1447,6 +1474,396 @@ function analyzeTypeAliasToIR(typeAlias, checker, file = "", extensionRegistry)
1447
1474
  }
1448
1475
  };
1449
1476
  }
1477
+ function makeAnalysisDiagnostic(code, message, primaryLocation, relatedLocations = []) {
1478
+ return {
1479
+ code,
1480
+ message,
1481
+ severity: "error",
1482
+ primaryLocation,
1483
+ relatedLocations
1484
+ };
1485
+ }
1486
+ function getLeadingParsedTags(node) {
1487
+ const sourceFile = node.getSourceFile();
1488
+ const sourceText = sourceFile.getFullText();
1489
+ const commentRanges = ts3.getLeadingCommentRanges(sourceText, node.getFullStart());
1490
+ if (commentRanges === void 0) {
1491
+ return [];
1492
+ }
1493
+ const parsedTags = [];
1494
+ for (const range of commentRanges) {
1495
+ if (range.kind !== ts3.SyntaxKind.MultiLineCommentTrivia) {
1496
+ continue;
1497
+ }
1498
+ const commentText = sourceText.slice(range.pos, range.end);
1499
+ if (!commentText.startsWith("/**")) {
1500
+ continue;
1501
+ }
1502
+ parsedTags.push(...parseCommentBlock2(commentText, { offset: range.pos }).tags);
1503
+ }
1504
+ return parsedTags;
1505
+ }
1506
+ function findDiscriminatorProperty(node, fieldName) {
1507
+ if (ts3.isClassDeclaration(node)) {
1508
+ for (const member of node.members) {
1509
+ if (ts3.isPropertyDeclaration(member) && ts3.isIdentifier(member.name) && member.name.text === fieldName) {
1510
+ return member;
1511
+ }
1512
+ }
1513
+ return null;
1514
+ }
1515
+ if (ts3.isInterfaceDeclaration(node)) {
1516
+ for (const member of node.members) {
1517
+ if (ts3.isPropertySignature(member) && ts3.isIdentifier(member.name) && member.name.text === fieldName) {
1518
+ return member;
1519
+ }
1520
+ }
1521
+ return null;
1522
+ }
1523
+ if (ts3.isTypeLiteralNode(node.type)) {
1524
+ for (const member of node.type.members) {
1525
+ if (ts3.isPropertySignature(member) && ts3.isIdentifier(member.name) && member.name.text === fieldName) {
1526
+ return member;
1527
+ }
1528
+ }
1529
+ }
1530
+ return null;
1531
+ }
1532
+ function isLocalTypeParameterName(node, typeParameterName) {
1533
+ return node.typeParameters?.some((typeParameter) => typeParameter.name.text === typeParameterName) ?? false;
1534
+ }
1535
+ function isNullishSemanticType(type) {
1536
+ if (type.flags & (ts3.TypeFlags.Null | ts3.TypeFlags.Undefined | ts3.TypeFlags.Void | ts3.TypeFlags.Unknown | ts3.TypeFlags.Any)) {
1537
+ return true;
1538
+ }
1539
+ return type.isUnion() && type.types.some((member) => isNullishSemanticType(member));
1540
+ }
1541
+ function isStringLikeSemanticType(type) {
1542
+ if (type.flags & ts3.TypeFlags.StringLike) {
1543
+ return true;
1544
+ }
1545
+ if (type.isUnion()) {
1546
+ return type.types.length > 0 && type.types.every((member) => isStringLikeSemanticType(member));
1547
+ }
1548
+ return false;
1549
+ }
1550
+ function extractDiscriminatorDirective(node, file, diagnostics) {
1551
+ const discriminatorTags = getLeadingParsedTags(node).filter(
1552
+ (tag) => tag.normalizedTagName === "discriminator"
1553
+ );
1554
+ if (discriminatorTags.length === 0) {
1555
+ return null;
1556
+ }
1557
+ const [firstTag, ...duplicateTags] = discriminatorTags;
1558
+ for (const _duplicateTag of duplicateTags) {
1559
+ diagnostics.push(
1560
+ makeAnalysisDiagnostic(
1561
+ "DUPLICATE_TAG",
1562
+ 'Duplicate "@discriminator" tag. Only one discriminator declaration is allowed per declaration.',
1563
+ provenanceForNode(node, file)
1564
+ )
1565
+ );
1566
+ }
1567
+ if (firstTag === void 0) {
1568
+ return null;
1569
+ }
1570
+ const firstTarget = firstTag.target;
1571
+ if (firstTarget?.path === null || firstTarget?.valid !== true) {
1572
+ diagnostics.push(
1573
+ makeAnalysisDiagnostic(
1574
+ "INVALID_TAG_ARGUMENT",
1575
+ 'Tag "@discriminator" requires a direct path target like ":kind".',
1576
+ provenanceForNode(node, file)
1577
+ )
1578
+ );
1579
+ return null;
1580
+ }
1581
+ if (firstTarget.path.segments.length !== 1) {
1582
+ diagnostics.push(
1583
+ makeAnalysisDiagnostic(
1584
+ "INVALID_TAG_ARGUMENT",
1585
+ 'Tag "@discriminator" only supports direct property targets in v1; nested paths are out of scope.',
1586
+ provenanceForNode(node, file)
1587
+ )
1588
+ );
1589
+ return null;
1590
+ }
1591
+ const typeParameterName = firstTag.argumentText.trim();
1592
+ if (!/^[A-Za-z_$][\w$]*$/u.test(typeParameterName)) {
1593
+ diagnostics.push(
1594
+ makeAnalysisDiagnostic(
1595
+ "INVALID_TAG_ARGUMENT",
1596
+ 'Tag "@discriminator" requires a local type parameter name as its source operand.',
1597
+ provenanceForNode(node, file)
1598
+ )
1599
+ );
1600
+ return null;
1601
+ }
1602
+ return {
1603
+ fieldName: firstTarget.path.segments[0] ?? firstTarget.rawText,
1604
+ typeParameterName,
1605
+ provenance: provenanceForNode(node, file)
1606
+ };
1607
+ }
1608
+ function validateDiscriminatorDirective(node, checker, file, diagnostics) {
1609
+ const directive = extractDiscriminatorDirective(node, file, diagnostics);
1610
+ if (directive === null) {
1611
+ return null;
1612
+ }
1613
+ if (!isLocalTypeParameterName(node, directive.typeParameterName)) {
1614
+ diagnostics.push(
1615
+ makeAnalysisDiagnostic(
1616
+ "INVALID_TAG_ARGUMENT",
1617
+ `Tag "@discriminator" references "${directive.typeParameterName}", but the source operand must be a type parameter declared on the same declaration.`,
1618
+ directive.provenance
1619
+ )
1620
+ );
1621
+ return null;
1622
+ }
1623
+ const propertyDecl = findDiscriminatorProperty(node, directive.fieldName);
1624
+ if (propertyDecl === null) {
1625
+ diagnostics.push(
1626
+ makeAnalysisDiagnostic(
1627
+ "UNKNOWN_PATH_TARGET",
1628
+ `Tag "@discriminator" targets "${directive.fieldName}", but no direct property with that name exists on this declaration.`,
1629
+ directive.provenance
1630
+ )
1631
+ );
1632
+ return null;
1633
+ }
1634
+ if (propertyDecl.questionToken !== void 0) {
1635
+ diagnostics.push(
1636
+ makeAnalysisDiagnostic(
1637
+ "TYPE_MISMATCH",
1638
+ `Discriminator field "${directive.fieldName}" must be required; optional discriminator fields are not supported.`,
1639
+ directive.provenance,
1640
+ [provenanceForNode(propertyDecl, file)]
1641
+ )
1642
+ );
1643
+ return null;
1644
+ }
1645
+ const propertyType = checker.getTypeAtLocation(propertyDecl);
1646
+ if (isNullishSemanticType(propertyType)) {
1647
+ diagnostics.push(
1648
+ makeAnalysisDiagnostic(
1649
+ "TYPE_MISMATCH",
1650
+ `Discriminator field "${directive.fieldName}" must not be nullable.`,
1651
+ directive.provenance,
1652
+ [provenanceForNode(propertyDecl, file)]
1653
+ )
1654
+ );
1655
+ return null;
1656
+ }
1657
+ if (!isStringLikeSemanticType(propertyType)) {
1658
+ diagnostics.push(
1659
+ makeAnalysisDiagnostic(
1660
+ "TYPE_MISMATCH",
1661
+ `Discriminator field "${directive.fieldName}" must be string-like.`,
1662
+ directive.provenance,
1663
+ [provenanceForNode(propertyDecl, file)]
1664
+ )
1665
+ );
1666
+ return null;
1667
+ }
1668
+ return directive;
1669
+ }
1670
+ function getConcreteTypeArgumentForDiscriminator(node, subjectType, checker, typeParameterName) {
1671
+ const typeParameterIndex = node.typeParameters?.findIndex(
1672
+ (typeParameter) => typeParameter.name.text === typeParameterName
1673
+ ) ?? -1;
1674
+ if (typeParameterIndex < 0) {
1675
+ return null;
1676
+ }
1677
+ const referenceTypeArguments = (isTypeReference(subjectType) ? subjectType.typeArguments : void 0) ?? subjectType.aliasTypeArguments;
1678
+ if (referenceTypeArguments?.[typeParameterIndex] !== void 0) {
1679
+ return referenceTypeArguments[typeParameterIndex] ?? null;
1680
+ }
1681
+ const localTypeParameter = node.typeParameters?.[typeParameterIndex];
1682
+ return localTypeParameter === void 0 ? null : checker.getTypeAtLocation(localTypeParameter);
1683
+ }
1684
+ function extractDeclarationApiName(node) {
1685
+ for (const tag of getLeadingParsedTags(node)) {
1686
+ if (tag.normalizedTagName !== "apiName") {
1687
+ continue;
1688
+ }
1689
+ if (tag.target === null && tag.argumentText.trim() !== "") {
1690
+ return tag.argumentText.trim();
1691
+ }
1692
+ if (tag.target?.kind === "variant" && tag.target.rawText === "singular") {
1693
+ const value = tag.argumentText.trim();
1694
+ if (value !== "") {
1695
+ return value;
1696
+ }
1697
+ }
1698
+ }
1699
+ return null;
1700
+ }
1701
+ function inferJsonFacingName(name) {
1702
+ 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();
1703
+ }
1704
+ function resolveNamedDiscriminatorDeclaration(type, checker, seen = /* @__PURE__ */ new Set()) {
1705
+ if (seen.has(type)) {
1706
+ return null;
1707
+ }
1708
+ seen.add(type);
1709
+ const symbol = type.aliasSymbol ?? type.getSymbol();
1710
+ if (symbol !== void 0) {
1711
+ const aliased = symbol.flags & ts3.SymbolFlags.Alias ? checker.getAliasedSymbol(symbol) : void 0;
1712
+ const targetSymbol = aliased ?? symbol;
1713
+ const declaration = targetSymbol.declarations?.find(
1714
+ (candidate) => ts3.isClassDeclaration(candidate) || ts3.isInterfaceDeclaration(candidate) || ts3.isTypeAliasDeclaration(candidate) || ts3.isEnumDeclaration(candidate)
1715
+ );
1716
+ if (declaration !== void 0) {
1717
+ if (ts3.isTypeAliasDeclaration(declaration) && ts3.isTypeReferenceNode(declaration.type) && checker.getTypeFromTypeNode(declaration.type) !== type) {
1718
+ return resolveNamedDiscriminatorDeclaration(
1719
+ checker.getTypeFromTypeNode(declaration.type),
1720
+ checker,
1721
+ seen
1722
+ );
1723
+ }
1724
+ return declaration;
1725
+ }
1726
+ }
1727
+ return null;
1728
+ }
1729
+ function resolveDiscriminatorValue(boundType, checker, provenance, diagnostics) {
1730
+ if (boundType === null) {
1731
+ diagnostics.push(
1732
+ makeAnalysisDiagnostic(
1733
+ "INVALID_TAG_ARGUMENT",
1734
+ "Discriminator resolution failed because no concrete type argument is available for the referenced type parameter.",
1735
+ provenance
1736
+ )
1737
+ );
1738
+ return null;
1739
+ }
1740
+ if (boundType.isStringLiteral()) {
1741
+ return boundType.value;
1742
+ }
1743
+ if (boundType.isUnion()) {
1744
+ const nonNullMembers = boundType.types.filter(
1745
+ (member) => !(member.flags & (ts3.TypeFlags.Null | ts3.TypeFlags.Undefined))
1746
+ );
1747
+ if (nonNullMembers.every((member) => member.isStringLiteral())) {
1748
+ diagnostics.push(
1749
+ makeAnalysisDiagnostic(
1750
+ "INVALID_TAG_ARGUMENT",
1751
+ "Discriminator resolution for unions of string literals is out of scope for v1.",
1752
+ provenance
1753
+ )
1754
+ );
1755
+ return null;
1756
+ }
1757
+ }
1758
+ const declaration = resolveNamedDiscriminatorDeclaration(boundType, checker);
1759
+ if (declaration !== null) {
1760
+ return extractDeclarationApiName(declaration) ?? inferJsonFacingName(getDeclarationName(declaration));
1761
+ }
1762
+ diagnostics.push(
1763
+ makeAnalysisDiagnostic(
1764
+ "INVALID_TAG_ARGUMENT",
1765
+ "Discriminator resolution could not derive a JSON-facing discriminator value from the referenced type argument.",
1766
+ provenance
1767
+ )
1768
+ );
1769
+ return null;
1770
+ }
1771
+ function getDeclarationName(node) {
1772
+ if (ts3.isClassDeclaration(node) || ts3.isInterfaceDeclaration(node) || ts3.isTypeAliasDeclaration(node) || ts3.isEnumDeclaration(node)) {
1773
+ return node.name?.text ?? "anonymous";
1774
+ }
1775
+ return "anonymous";
1776
+ }
1777
+ function applyDeclarationDiscriminatorToFields(fields, node, subjectType, checker, file, diagnostics) {
1778
+ const directive = validateDiscriminatorDirective(node, checker, file, diagnostics);
1779
+ if (directive === null) {
1780
+ return [...fields];
1781
+ }
1782
+ const discriminatorValue = resolveDiscriminatorValue(
1783
+ getConcreteTypeArgumentForDiscriminator(
1784
+ node,
1785
+ subjectType,
1786
+ checker,
1787
+ directive.typeParameterName
1788
+ ),
1789
+ checker,
1790
+ directive.provenance,
1791
+ diagnostics
1792
+ );
1793
+ if (discriminatorValue === null) {
1794
+ return [...fields];
1795
+ }
1796
+ return fields.map(
1797
+ (field) => field.name === directive.fieldName ? {
1798
+ ...field,
1799
+ type: {
1800
+ kind: "enum",
1801
+ members: [{ value: discriminatorValue }]
1802
+ }
1803
+ } : field
1804
+ );
1805
+ }
1806
+ function buildInstantiatedReferenceName(baseName, typeArguments, checker) {
1807
+ const renderedArguments = typeArguments.map(
1808
+ (typeArgument) => checker.typeToString(typeArgument).replace(/[^A-Za-z0-9]+/g, "_").replace(/^_+|_+$/g, "")
1809
+ ).filter((value) => value !== "");
1810
+ return renderedArguments.length === 0 ? baseName : `${baseName}__${renderedArguments.join("__")}`;
1811
+ }
1812
+ function extractReferenceTypeArguments(type, checker, file, typeRegistry, visiting, sourceNode, extensionRegistry, diagnostics) {
1813
+ const typeNode = sourceNode === void 0 ? void 0 : extractTypeNodeFromSource(sourceNode);
1814
+ if (typeNode === void 0) {
1815
+ return [];
1816
+ }
1817
+ const resolvedTypeNode = resolveAliasedTypeNode(typeNode, checker);
1818
+ if (!ts3.isTypeReferenceNode(resolvedTypeNode) || resolvedTypeNode.typeArguments === void 0) {
1819
+ return [];
1820
+ }
1821
+ return resolvedTypeNode.typeArguments.map((argumentNode) => {
1822
+ const argumentType = checker.getTypeFromTypeNode(argumentNode);
1823
+ return {
1824
+ tsType: argumentType,
1825
+ typeNode: resolveTypeNode(
1826
+ argumentType,
1827
+ checker,
1828
+ file,
1829
+ typeRegistry,
1830
+ visiting,
1831
+ argumentNode,
1832
+ extensionRegistry,
1833
+ diagnostics
1834
+ )
1835
+ };
1836
+ });
1837
+ }
1838
+ function applyDiscriminatorToObjectProperties(properties, node, subjectType, checker, file, diagnostics) {
1839
+ const directive = validateDiscriminatorDirective(node, checker, file, diagnostics);
1840
+ if (directive === null) {
1841
+ return properties;
1842
+ }
1843
+ const discriminatorValue = resolveDiscriminatorValue(
1844
+ getConcreteTypeArgumentForDiscriminator(
1845
+ node,
1846
+ subjectType,
1847
+ checker,
1848
+ directive.typeParameterName
1849
+ ),
1850
+ checker,
1851
+ directive.provenance,
1852
+ diagnostics
1853
+ );
1854
+ if (discriminatorValue === null) {
1855
+ return properties;
1856
+ }
1857
+ return properties.map(
1858
+ (property) => property.name === directive.fieldName ? {
1859
+ ...property,
1860
+ type: {
1861
+ kind: "enum",
1862
+ members: [{ value: discriminatorValue }]
1863
+ }
1864
+ } : property
1865
+ );
1866
+ }
1450
1867
  function analyzeFieldToIR(prop, checker, file, typeRegistry, visiting, diagnostics, hostType, extensionRegistry) {
1451
1868
  if (!ts3.isIdentifier(prop.name)) {
1452
1869
  return null;
@@ -1735,6 +2152,7 @@ function resolveTypeNode(type, checker, file, typeRegistry, visiting, sourceNode
1735
2152
  file,
1736
2153
  typeRegistry,
1737
2154
  visiting,
2155
+ sourceNode,
1738
2156
  extensionRegistry,
1739
2157
  diagnostics
1740
2158
  );
@@ -1998,35 +2416,60 @@ function typeNodeContainsReference(type, targetName) {
1998
2416
  }
1999
2417
  }
2000
2418
  }
2001
- function resolveObjectType(type, checker, file, typeRegistry, visiting, extensionRegistry, diagnostics) {
2419
+ function resolveObjectType(type, checker, file, typeRegistry, visiting, sourceNode, extensionRegistry, diagnostics) {
2420
+ const collectedDiagnostics = diagnostics ?? [];
2002
2421
  const typeName = getNamedTypeName(type);
2003
2422
  const namedTypeName = typeName ?? void 0;
2004
2423
  const namedDecl = getNamedTypeDeclaration(type);
2005
- const shouldRegisterNamedType = namedTypeName !== void 0 && !(namedTypeName === "Record" && namedDecl?.getSourceFile().fileName !== file);
2424
+ const referenceTypeArguments = extractReferenceTypeArguments(
2425
+ type,
2426
+ checker,
2427
+ file,
2428
+ typeRegistry,
2429
+ visiting,
2430
+ sourceNode,
2431
+ extensionRegistry,
2432
+ collectedDiagnostics
2433
+ );
2434
+ const instantiatedTypeName = namedTypeName !== void 0 && referenceTypeArguments.length > 0 ? buildInstantiatedReferenceName(
2435
+ namedTypeName,
2436
+ referenceTypeArguments.map((argument) => argument.tsType),
2437
+ checker
2438
+ ) : void 0;
2439
+ const registryTypeName = instantiatedTypeName ?? namedTypeName;
2440
+ const shouldRegisterNamedType = registryTypeName !== void 0 && !(registryTypeName === "Record" && namedDecl?.getSourceFile().fileName !== file);
2006
2441
  const clearNamedTypeRegistration = () => {
2007
- if (namedTypeName === void 0 || !shouldRegisterNamedType) {
2442
+ if (registryTypeName === void 0 || !shouldRegisterNamedType) {
2008
2443
  return;
2009
2444
  }
2010
- Reflect.deleteProperty(typeRegistry, namedTypeName);
2445
+ Reflect.deleteProperty(typeRegistry, registryTypeName);
2011
2446
  };
2012
2447
  if (visiting.has(type)) {
2013
- if (namedTypeName !== void 0 && shouldRegisterNamedType) {
2014
- return { kind: "reference", name: namedTypeName, typeArguments: [] };
2448
+ if (registryTypeName !== void 0 && shouldRegisterNamedType) {
2449
+ return {
2450
+ kind: "reference",
2451
+ name: registryTypeName,
2452
+ typeArguments: referenceTypeArguments.map((argument) => argument.typeNode)
2453
+ };
2015
2454
  }
2016
2455
  return { kind: "object", properties: [], additionalProperties: false };
2017
2456
  }
2018
- if (namedTypeName !== void 0 && shouldRegisterNamedType && !typeRegistry[namedTypeName]) {
2019
- typeRegistry[namedTypeName] = {
2020
- name: namedTypeName,
2457
+ if (registryTypeName !== void 0 && shouldRegisterNamedType && !typeRegistry[registryTypeName]) {
2458
+ typeRegistry[registryTypeName] = {
2459
+ name: registryTypeName,
2021
2460
  type: RESOLVING_TYPE_PLACEHOLDER,
2022
2461
  provenance: provenanceForDeclaration(namedDecl, file)
2023
2462
  };
2024
2463
  }
2025
2464
  visiting.add(type);
2026
- if (namedTypeName !== void 0 && shouldRegisterNamedType && typeRegistry[namedTypeName]?.type !== void 0) {
2027
- if (typeRegistry[namedTypeName].type !== RESOLVING_TYPE_PLACEHOLDER) {
2465
+ if (registryTypeName !== void 0 && shouldRegisterNamedType && typeRegistry[registryTypeName]?.type !== void 0) {
2466
+ if (typeRegistry[registryTypeName].type !== RESOLVING_TYPE_PLACEHOLDER) {
2028
2467
  visiting.delete(type);
2029
- return { kind: "reference", name: namedTypeName, typeArguments: [] };
2468
+ return {
2469
+ kind: "reference",
2470
+ name: registryTypeName,
2471
+ typeArguments: referenceTypeArguments.map((argument) => argument.typeNode)
2472
+ };
2030
2473
  }
2031
2474
  }
2032
2475
  const recordNode = tryResolveRecordType(
@@ -2036,24 +2479,28 @@ function resolveObjectType(type, checker, file, typeRegistry, visiting, extensio
2036
2479
  typeRegistry,
2037
2480
  visiting,
2038
2481
  extensionRegistry,
2039
- diagnostics
2482
+ collectedDiagnostics
2040
2483
  );
2041
2484
  if (recordNode) {
2042
2485
  visiting.delete(type);
2043
- if (namedTypeName !== void 0 && shouldRegisterNamedType) {
2044
- const isRecursiveRecord = typeNodeContainsReference(recordNode.valueType, namedTypeName);
2486
+ if (registryTypeName !== void 0 && shouldRegisterNamedType) {
2487
+ const isRecursiveRecord = typeNodeContainsReference(recordNode.valueType, registryTypeName);
2045
2488
  if (!isRecursiveRecord) {
2046
2489
  clearNamedTypeRegistration();
2047
2490
  return recordNode;
2048
2491
  }
2049
2492
  const annotations = namedDecl ? extractJSDocAnnotationNodes(namedDecl, file, makeParseOptions(extensionRegistry)) : void 0;
2050
- typeRegistry[namedTypeName] = {
2051
- name: namedTypeName,
2493
+ typeRegistry[registryTypeName] = {
2494
+ name: registryTypeName,
2052
2495
  type: recordNode,
2053
2496
  ...annotations !== void 0 && annotations.length > 0 && { annotations },
2054
2497
  provenance: provenanceForDeclaration(namedDecl, file)
2055
2498
  };
2056
- return { kind: "reference", name: namedTypeName, typeArguments: [] };
2499
+ return {
2500
+ kind: "reference",
2501
+ name: registryTypeName,
2502
+ typeArguments: referenceTypeArguments.map((argument) => argument.typeNode)
2503
+ };
2057
2504
  }
2058
2505
  return recordNode;
2059
2506
  }
@@ -2064,7 +2511,7 @@ function resolveObjectType(type, checker, file, typeRegistry, visiting, extensio
2064
2511
  file,
2065
2512
  typeRegistry,
2066
2513
  visiting,
2067
- diagnostics ?? [],
2514
+ collectedDiagnostics,
2068
2515
  extensionRegistry
2069
2516
  );
2070
2517
  for (const prop of type.getProperties()) {
@@ -2080,7 +2527,7 @@ function resolveObjectType(type, checker, file, typeRegistry, visiting, extensio
2080
2527
  visiting,
2081
2528
  declaration,
2082
2529
  extensionRegistry,
2083
- diagnostics
2530
+ collectedDiagnostics
2084
2531
  );
2085
2532
  const fieldNodeInfo = fieldInfoMap?.get(prop.name);
2086
2533
  properties.push({
@@ -2095,18 +2542,29 @@ function resolveObjectType(type, checker, file, typeRegistry, visiting, extensio
2095
2542
  visiting.delete(type);
2096
2543
  const objectNode = {
2097
2544
  kind: "object",
2098
- properties,
2545
+ properties: namedDecl !== void 0 && (ts3.isClassDeclaration(namedDecl) || ts3.isInterfaceDeclaration(namedDecl) || ts3.isTypeAliasDeclaration(namedDecl)) ? applyDiscriminatorToObjectProperties(
2546
+ properties,
2547
+ namedDecl,
2548
+ type,
2549
+ checker,
2550
+ file,
2551
+ collectedDiagnostics
2552
+ ) : properties,
2099
2553
  additionalProperties: true
2100
2554
  };
2101
- if (namedTypeName !== void 0 && shouldRegisterNamedType) {
2555
+ if (registryTypeName !== void 0 && shouldRegisterNamedType) {
2102
2556
  const annotations = namedDecl ? extractJSDocAnnotationNodes(namedDecl, file, makeParseOptions(extensionRegistry)) : void 0;
2103
- typeRegistry[namedTypeName] = {
2104
- name: namedTypeName,
2557
+ typeRegistry[registryTypeName] = {
2558
+ name: registryTypeName,
2105
2559
  type: objectNode,
2106
2560
  ...annotations !== void 0 && annotations.length > 0 && { annotations },
2107
2561
  provenance: provenanceForDeclaration(namedDecl, file)
2108
2562
  };
2109
- return { kind: "reference", name: namedTypeName, typeArguments: [] };
2563
+ return {
2564
+ kind: "reference",
2565
+ name: registryTypeName,
2566
+ typeArguments: referenceTypeArguments.map((argument) => argument.typeNode)
2567
+ };
2110
2568
  }
2111
2569
  return objectNode;
2112
2570
  }
@@ -2367,6 +2825,18 @@ function detectFormSpecReference(typeNode) {
2367
2825
  }
2368
2826
 
2369
2827
  // src/analyzer/program.ts
2828
+ function createProgramContextFromProgram(program, filePath) {
2829
+ const absolutePath = path.resolve(filePath);
2830
+ const sourceFile = program.getSourceFile(absolutePath) ?? program.getSourceFile(filePath);
2831
+ if (!sourceFile) {
2832
+ throw new Error(`Could not find source file in provided program: ${absolutePath}`);
2833
+ }
2834
+ return {
2835
+ program,
2836
+ checker: program.getTypeChecker(),
2837
+ sourceFile
2838
+ };
2839
+ }
2370
2840
  function createProgramContext(filePath) {
2371
2841
  const absolutePath = path.resolve(filePath);
2372
2842
  const fileDir = path.dirname(absolutePath);
@@ -2435,6 +2905,36 @@ function findInterfaceByName(sourceFile, interfaceName) {
2435
2905
  function findTypeAliasByName(sourceFile, aliasName) {
2436
2906
  return findNodeByName(sourceFile, aliasName, ts4.isTypeAliasDeclaration, (n) => n.name.text);
2437
2907
  }
2908
+ function analyzeNamedTypeToIRFromProgramContext(ctx, filePath, typeName, extensionRegistry) {
2909
+ const analysisFilePath = path.resolve(filePath);
2910
+ const classDecl = findClassByName(ctx.sourceFile, typeName);
2911
+ if (classDecl !== null) {
2912
+ return analyzeClassToIR(classDecl, ctx.checker, analysisFilePath, extensionRegistry);
2913
+ }
2914
+ const interfaceDecl = findInterfaceByName(ctx.sourceFile, typeName);
2915
+ if (interfaceDecl !== null) {
2916
+ return analyzeInterfaceToIR(interfaceDecl, ctx.checker, analysisFilePath, extensionRegistry);
2917
+ }
2918
+ const typeAlias = findTypeAliasByName(ctx.sourceFile, typeName);
2919
+ if (typeAlias !== null) {
2920
+ const result = analyzeTypeAliasToIR(
2921
+ typeAlias,
2922
+ ctx.checker,
2923
+ analysisFilePath,
2924
+ extensionRegistry
2925
+ );
2926
+ if (result.ok) {
2927
+ return result.analysis;
2928
+ }
2929
+ throw new Error(result.error);
2930
+ }
2931
+ throw new Error(
2932
+ `Type "${typeName}" not found as a class, interface, or type alias in ${analysisFilePath}`
2933
+ );
2934
+ }
2935
+
2936
+ // src/generators/class-schema.ts
2937
+ import "typescript";
2438
2938
 
2439
2939
  // src/json-schema/ir-generator.ts
2440
2940
  function makeContext(options) {
@@ -3290,7 +3790,23 @@ import { IR_VERSION as IR_VERSION3 } from "@formspec/core/internals";
3290
3790
  function typeToJsonSchema(type, checker) {
3291
3791
  const typeRegistry = {};
3292
3792
  const visiting = /* @__PURE__ */ new Set();
3293
- const typeNode = resolveTypeNode(type, checker, "", typeRegistry, visiting);
3793
+ const diagnostics = [];
3794
+ const typeNode = resolveTypeNode(
3795
+ type,
3796
+ checker,
3797
+ "",
3798
+ typeRegistry,
3799
+ visiting,
3800
+ void 0,
3801
+ void 0,
3802
+ diagnostics
3803
+ );
3804
+ if (diagnostics.length > 0) {
3805
+ const diagnosticDetails = diagnostics.map((diagnostic) => `${diagnostic.code}: ${diagnostic.message}`).join("; ");
3806
+ throw new Error(
3807
+ `FormSpec validation failed while resolving method schema types. ${diagnosticDetails}`
3808
+ );
3809
+ }
3294
3810
  const fieldProvenance = { surface: "tsdoc", file: "", line: 0, column: 0 };
3295
3811
  const ir = {
3296
3812
  kind: "form-ir",
@@ -3390,12 +3906,14 @@ function collectFormSpecReferences(methods) {
3390
3906
  export {
3391
3907
  analyzeClassToIR,
3392
3908
  analyzeInterfaceToIR,
3909
+ analyzeNamedTypeToIRFromProgramContext,
3393
3910
  analyzeTypeAliasToIR,
3394
3911
  canonicalizeChainDSL,
3395
3912
  canonicalizeTSDoc,
3396
3913
  collectFormSpecReferences,
3397
3914
  createExtensionRegistry,
3398
3915
  createProgramContext,
3916
+ createProgramContextFromProgram,
3399
3917
  findClassByName,
3400
3918
  findInterfaceByName,
3401
3919
  findTypeAliasByName,