@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.
package/dist/index.js CHANGED
@@ -1212,6 +1212,9 @@ import * as path from "path";
1212
1212
 
1213
1213
  // src/analyzer/class-analyzer.ts
1214
1214
  import * as ts3 from "typescript";
1215
+ import {
1216
+ parseCommentBlock as parseCommentBlock2
1217
+ } from "@formspec/analysis/internal";
1215
1218
 
1216
1219
  // src/analyzer/jsdoc-constraints.ts
1217
1220
  import * as ts2 from "typescript";
@@ -2159,9 +2162,17 @@ function analyzeClassToIR(classDecl, checker, file = "", extensionRegistry) {
2159
2162
  }
2160
2163
  }
2161
2164
  }
2165
+ const specializedFields = applyDeclarationDiscriminatorToFields(
2166
+ fields,
2167
+ classDecl,
2168
+ classType,
2169
+ checker,
2170
+ file,
2171
+ diagnostics
2172
+ );
2162
2173
  return {
2163
2174
  name,
2164
- fields,
2175
+ fields: specializedFields,
2165
2176
  fieldLayouts,
2166
2177
  typeRegistry,
2167
2178
  ...annotations.length > 0 && { annotations },
@@ -2201,10 +2212,18 @@ function analyzeInterfaceToIR(interfaceDecl, checker, file = "", extensionRegist
2201
2212
  }
2202
2213
  }
2203
2214
  }
2204
- const fieldLayouts = fields.map(() => ({}));
2215
+ const specializedFields = applyDeclarationDiscriminatorToFields(
2216
+ fields,
2217
+ interfaceDecl,
2218
+ interfaceType,
2219
+ checker,
2220
+ file,
2221
+ diagnostics
2222
+ );
2223
+ const fieldLayouts = specializedFields.map(() => ({}));
2205
2224
  return {
2206
2225
  name,
2207
- fields,
2226
+ fields: specializedFields,
2208
2227
  fieldLayouts,
2209
2228
  typeRegistry,
2210
2229
  ...annotations.length > 0 && { annotations },
@@ -2253,12 +2272,20 @@ function analyzeTypeAliasToIR(typeAlias, checker, file = "", extensionRegistry)
2253
2272
  }
2254
2273
  }
2255
2274
  }
2275
+ const specializedFields = applyDeclarationDiscriminatorToFields(
2276
+ fields,
2277
+ typeAlias,
2278
+ aliasType,
2279
+ checker,
2280
+ file,
2281
+ diagnostics
2282
+ );
2256
2283
  return {
2257
2284
  ok: true,
2258
2285
  analysis: {
2259
2286
  name,
2260
- fields,
2261
- fieldLayouts: fields.map(() => ({})),
2287
+ fields: specializedFields,
2288
+ fieldLayouts: specializedFields.map(() => ({})),
2262
2289
  typeRegistry,
2263
2290
  ...annotations.length > 0 && { annotations },
2264
2291
  ...diagnostics.length > 0 && { diagnostics },
@@ -2267,6 +2294,396 @@ function analyzeTypeAliasToIR(typeAlias, checker, file = "", extensionRegistry)
2267
2294
  }
2268
2295
  };
2269
2296
  }
2297
+ function makeAnalysisDiagnostic(code, message, primaryLocation, relatedLocations = []) {
2298
+ return {
2299
+ code,
2300
+ message,
2301
+ severity: "error",
2302
+ primaryLocation,
2303
+ relatedLocations
2304
+ };
2305
+ }
2306
+ function getLeadingParsedTags(node) {
2307
+ const sourceFile = node.getSourceFile();
2308
+ const sourceText = sourceFile.getFullText();
2309
+ const commentRanges = ts3.getLeadingCommentRanges(sourceText, node.getFullStart());
2310
+ if (commentRanges === void 0) {
2311
+ return [];
2312
+ }
2313
+ const parsedTags = [];
2314
+ for (const range of commentRanges) {
2315
+ if (range.kind !== ts3.SyntaxKind.MultiLineCommentTrivia) {
2316
+ continue;
2317
+ }
2318
+ const commentText = sourceText.slice(range.pos, range.end);
2319
+ if (!commentText.startsWith("/**")) {
2320
+ continue;
2321
+ }
2322
+ parsedTags.push(...parseCommentBlock2(commentText, { offset: range.pos }).tags);
2323
+ }
2324
+ return parsedTags;
2325
+ }
2326
+ function findDiscriminatorProperty(node, fieldName) {
2327
+ if (ts3.isClassDeclaration(node)) {
2328
+ for (const member of node.members) {
2329
+ if (ts3.isPropertyDeclaration(member) && ts3.isIdentifier(member.name) && member.name.text === fieldName) {
2330
+ return member;
2331
+ }
2332
+ }
2333
+ return null;
2334
+ }
2335
+ if (ts3.isInterfaceDeclaration(node)) {
2336
+ for (const member of node.members) {
2337
+ if (ts3.isPropertySignature(member) && ts3.isIdentifier(member.name) && member.name.text === fieldName) {
2338
+ return member;
2339
+ }
2340
+ }
2341
+ return null;
2342
+ }
2343
+ if (ts3.isTypeLiteralNode(node.type)) {
2344
+ for (const member of node.type.members) {
2345
+ if (ts3.isPropertySignature(member) && ts3.isIdentifier(member.name) && member.name.text === fieldName) {
2346
+ return member;
2347
+ }
2348
+ }
2349
+ }
2350
+ return null;
2351
+ }
2352
+ function isLocalTypeParameterName(node, typeParameterName) {
2353
+ return node.typeParameters?.some((typeParameter) => typeParameter.name.text === typeParameterName) ?? false;
2354
+ }
2355
+ function isNullishSemanticType(type) {
2356
+ if (type.flags & (ts3.TypeFlags.Null | ts3.TypeFlags.Undefined | ts3.TypeFlags.Void | ts3.TypeFlags.Unknown | ts3.TypeFlags.Any)) {
2357
+ return true;
2358
+ }
2359
+ return type.isUnion() && type.types.some((member) => isNullishSemanticType(member));
2360
+ }
2361
+ function isStringLikeSemanticType(type) {
2362
+ if (type.flags & ts3.TypeFlags.StringLike) {
2363
+ return true;
2364
+ }
2365
+ if (type.isUnion()) {
2366
+ return type.types.length > 0 && type.types.every((member) => isStringLikeSemanticType(member));
2367
+ }
2368
+ return false;
2369
+ }
2370
+ function extractDiscriminatorDirective(node, file, diagnostics) {
2371
+ const discriminatorTags = getLeadingParsedTags(node).filter(
2372
+ (tag) => tag.normalizedTagName === "discriminator"
2373
+ );
2374
+ if (discriminatorTags.length === 0) {
2375
+ return null;
2376
+ }
2377
+ const [firstTag, ...duplicateTags] = discriminatorTags;
2378
+ for (const _duplicateTag of duplicateTags) {
2379
+ diagnostics.push(
2380
+ makeAnalysisDiagnostic(
2381
+ "DUPLICATE_TAG",
2382
+ 'Duplicate "@discriminator" tag. Only one discriminator declaration is allowed per declaration.',
2383
+ provenanceForNode(node, file)
2384
+ )
2385
+ );
2386
+ }
2387
+ if (firstTag === void 0) {
2388
+ return null;
2389
+ }
2390
+ const firstTarget = firstTag.target;
2391
+ if (firstTarget?.path === null || firstTarget?.valid !== true) {
2392
+ diagnostics.push(
2393
+ makeAnalysisDiagnostic(
2394
+ "INVALID_TAG_ARGUMENT",
2395
+ 'Tag "@discriminator" requires a direct path target like ":kind".',
2396
+ provenanceForNode(node, file)
2397
+ )
2398
+ );
2399
+ return null;
2400
+ }
2401
+ if (firstTarget.path.segments.length !== 1) {
2402
+ diagnostics.push(
2403
+ makeAnalysisDiagnostic(
2404
+ "INVALID_TAG_ARGUMENT",
2405
+ 'Tag "@discriminator" only supports direct property targets in v1; nested paths are out of scope.',
2406
+ provenanceForNode(node, file)
2407
+ )
2408
+ );
2409
+ return null;
2410
+ }
2411
+ const typeParameterName = firstTag.argumentText.trim();
2412
+ if (!/^[A-Za-z_$][\w$]*$/u.test(typeParameterName)) {
2413
+ diagnostics.push(
2414
+ makeAnalysisDiagnostic(
2415
+ "INVALID_TAG_ARGUMENT",
2416
+ 'Tag "@discriminator" requires a local type parameter name as its source operand.',
2417
+ provenanceForNode(node, file)
2418
+ )
2419
+ );
2420
+ return null;
2421
+ }
2422
+ return {
2423
+ fieldName: firstTarget.path.segments[0] ?? firstTarget.rawText,
2424
+ typeParameterName,
2425
+ provenance: provenanceForNode(node, file)
2426
+ };
2427
+ }
2428
+ function validateDiscriminatorDirective(node, checker, file, diagnostics) {
2429
+ const directive = extractDiscriminatorDirective(node, file, diagnostics);
2430
+ if (directive === null) {
2431
+ return null;
2432
+ }
2433
+ if (!isLocalTypeParameterName(node, directive.typeParameterName)) {
2434
+ diagnostics.push(
2435
+ makeAnalysisDiagnostic(
2436
+ "INVALID_TAG_ARGUMENT",
2437
+ `Tag "@discriminator" references "${directive.typeParameterName}", but the source operand must be a type parameter declared on the same declaration.`,
2438
+ directive.provenance
2439
+ )
2440
+ );
2441
+ return null;
2442
+ }
2443
+ const propertyDecl = findDiscriminatorProperty(node, directive.fieldName);
2444
+ if (propertyDecl === null) {
2445
+ diagnostics.push(
2446
+ makeAnalysisDiagnostic(
2447
+ "UNKNOWN_PATH_TARGET",
2448
+ `Tag "@discriminator" targets "${directive.fieldName}", but no direct property with that name exists on this declaration.`,
2449
+ directive.provenance
2450
+ )
2451
+ );
2452
+ return null;
2453
+ }
2454
+ if (propertyDecl.questionToken !== void 0) {
2455
+ diagnostics.push(
2456
+ makeAnalysisDiagnostic(
2457
+ "TYPE_MISMATCH",
2458
+ `Discriminator field "${directive.fieldName}" must be required; optional discriminator fields are not supported.`,
2459
+ directive.provenance,
2460
+ [provenanceForNode(propertyDecl, file)]
2461
+ )
2462
+ );
2463
+ return null;
2464
+ }
2465
+ const propertyType = checker.getTypeAtLocation(propertyDecl);
2466
+ if (isNullishSemanticType(propertyType)) {
2467
+ diagnostics.push(
2468
+ makeAnalysisDiagnostic(
2469
+ "TYPE_MISMATCH",
2470
+ `Discriminator field "${directive.fieldName}" must not be nullable.`,
2471
+ directive.provenance,
2472
+ [provenanceForNode(propertyDecl, file)]
2473
+ )
2474
+ );
2475
+ return null;
2476
+ }
2477
+ if (!isStringLikeSemanticType(propertyType)) {
2478
+ diagnostics.push(
2479
+ makeAnalysisDiagnostic(
2480
+ "TYPE_MISMATCH",
2481
+ `Discriminator field "${directive.fieldName}" must be string-like.`,
2482
+ directive.provenance,
2483
+ [provenanceForNode(propertyDecl, file)]
2484
+ )
2485
+ );
2486
+ return null;
2487
+ }
2488
+ return directive;
2489
+ }
2490
+ function getConcreteTypeArgumentForDiscriminator(node, subjectType, checker, typeParameterName) {
2491
+ const typeParameterIndex = node.typeParameters?.findIndex(
2492
+ (typeParameter) => typeParameter.name.text === typeParameterName
2493
+ ) ?? -1;
2494
+ if (typeParameterIndex < 0) {
2495
+ return null;
2496
+ }
2497
+ const referenceTypeArguments = (isTypeReference(subjectType) ? subjectType.typeArguments : void 0) ?? subjectType.aliasTypeArguments;
2498
+ if (referenceTypeArguments?.[typeParameterIndex] !== void 0) {
2499
+ return referenceTypeArguments[typeParameterIndex] ?? null;
2500
+ }
2501
+ const localTypeParameter = node.typeParameters?.[typeParameterIndex];
2502
+ return localTypeParameter === void 0 ? null : checker.getTypeAtLocation(localTypeParameter);
2503
+ }
2504
+ function extractDeclarationApiName(node) {
2505
+ for (const tag of getLeadingParsedTags(node)) {
2506
+ if (tag.normalizedTagName !== "apiName") {
2507
+ continue;
2508
+ }
2509
+ if (tag.target === null && tag.argumentText.trim() !== "") {
2510
+ return tag.argumentText.trim();
2511
+ }
2512
+ if (tag.target?.kind === "variant" && tag.target.rawText === "singular") {
2513
+ const value = tag.argumentText.trim();
2514
+ if (value !== "") {
2515
+ return value;
2516
+ }
2517
+ }
2518
+ }
2519
+ return null;
2520
+ }
2521
+ function inferJsonFacingName(name) {
2522
+ 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();
2523
+ }
2524
+ function resolveNamedDiscriminatorDeclaration(type, checker, seen = /* @__PURE__ */ new Set()) {
2525
+ if (seen.has(type)) {
2526
+ return null;
2527
+ }
2528
+ seen.add(type);
2529
+ const symbol = type.aliasSymbol ?? type.getSymbol();
2530
+ if (symbol !== void 0) {
2531
+ const aliased = symbol.flags & ts3.SymbolFlags.Alias ? checker.getAliasedSymbol(symbol) : void 0;
2532
+ const targetSymbol = aliased ?? symbol;
2533
+ const declaration = targetSymbol.declarations?.find(
2534
+ (candidate) => ts3.isClassDeclaration(candidate) || ts3.isInterfaceDeclaration(candidate) || ts3.isTypeAliasDeclaration(candidate) || ts3.isEnumDeclaration(candidate)
2535
+ );
2536
+ if (declaration !== void 0) {
2537
+ if (ts3.isTypeAliasDeclaration(declaration) && ts3.isTypeReferenceNode(declaration.type) && checker.getTypeFromTypeNode(declaration.type) !== type) {
2538
+ return resolveNamedDiscriminatorDeclaration(
2539
+ checker.getTypeFromTypeNode(declaration.type),
2540
+ checker,
2541
+ seen
2542
+ );
2543
+ }
2544
+ return declaration;
2545
+ }
2546
+ }
2547
+ return null;
2548
+ }
2549
+ function resolveDiscriminatorValue(boundType, checker, provenance, diagnostics) {
2550
+ if (boundType === null) {
2551
+ diagnostics.push(
2552
+ makeAnalysisDiagnostic(
2553
+ "INVALID_TAG_ARGUMENT",
2554
+ "Discriminator resolution failed because no concrete type argument is available for the referenced type parameter.",
2555
+ provenance
2556
+ )
2557
+ );
2558
+ return null;
2559
+ }
2560
+ if (boundType.isStringLiteral()) {
2561
+ return boundType.value;
2562
+ }
2563
+ if (boundType.isUnion()) {
2564
+ const nonNullMembers = boundType.types.filter(
2565
+ (member) => !(member.flags & (ts3.TypeFlags.Null | ts3.TypeFlags.Undefined))
2566
+ );
2567
+ if (nonNullMembers.every((member) => member.isStringLiteral())) {
2568
+ diagnostics.push(
2569
+ makeAnalysisDiagnostic(
2570
+ "INVALID_TAG_ARGUMENT",
2571
+ "Discriminator resolution for unions of string literals is out of scope for v1.",
2572
+ provenance
2573
+ )
2574
+ );
2575
+ return null;
2576
+ }
2577
+ }
2578
+ const declaration = resolveNamedDiscriminatorDeclaration(boundType, checker);
2579
+ if (declaration !== null) {
2580
+ return extractDeclarationApiName(declaration) ?? inferJsonFacingName(getDeclarationName(declaration));
2581
+ }
2582
+ diagnostics.push(
2583
+ makeAnalysisDiagnostic(
2584
+ "INVALID_TAG_ARGUMENT",
2585
+ "Discriminator resolution could not derive a JSON-facing discriminator value from the referenced type argument.",
2586
+ provenance
2587
+ )
2588
+ );
2589
+ return null;
2590
+ }
2591
+ function getDeclarationName(node) {
2592
+ if (ts3.isClassDeclaration(node) || ts3.isInterfaceDeclaration(node) || ts3.isTypeAliasDeclaration(node) || ts3.isEnumDeclaration(node)) {
2593
+ return node.name?.text ?? "anonymous";
2594
+ }
2595
+ return "anonymous";
2596
+ }
2597
+ function applyDeclarationDiscriminatorToFields(fields, node, subjectType, checker, file, diagnostics) {
2598
+ const directive = validateDiscriminatorDirective(node, checker, file, diagnostics);
2599
+ if (directive === null) {
2600
+ return [...fields];
2601
+ }
2602
+ const discriminatorValue = resolveDiscriminatorValue(
2603
+ getConcreteTypeArgumentForDiscriminator(
2604
+ node,
2605
+ subjectType,
2606
+ checker,
2607
+ directive.typeParameterName
2608
+ ),
2609
+ checker,
2610
+ directive.provenance,
2611
+ diagnostics
2612
+ );
2613
+ if (discriminatorValue === null) {
2614
+ return [...fields];
2615
+ }
2616
+ return fields.map(
2617
+ (field) => field.name === directive.fieldName ? {
2618
+ ...field,
2619
+ type: {
2620
+ kind: "enum",
2621
+ members: [{ value: discriminatorValue }]
2622
+ }
2623
+ } : field
2624
+ );
2625
+ }
2626
+ function buildInstantiatedReferenceName(baseName, typeArguments, checker) {
2627
+ const renderedArguments = typeArguments.map(
2628
+ (typeArgument) => checker.typeToString(typeArgument).replace(/[^A-Za-z0-9]+/g, "_").replace(/^_+|_+$/g, "")
2629
+ ).filter((value) => value !== "");
2630
+ return renderedArguments.length === 0 ? baseName : `${baseName}__${renderedArguments.join("__")}`;
2631
+ }
2632
+ function extractReferenceTypeArguments(type, checker, file, typeRegistry, visiting, sourceNode, extensionRegistry, diagnostics) {
2633
+ const typeNode = sourceNode === void 0 ? void 0 : extractTypeNodeFromSource(sourceNode);
2634
+ if (typeNode === void 0) {
2635
+ return [];
2636
+ }
2637
+ const resolvedTypeNode = resolveAliasedTypeNode(typeNode, checker);
2638
+ if (!ts3.isTypeReferenceNode(resolvedTypeNode) || resolvedTypeNode.typeArguments === void 0) {
2639
+ return [];
2640
+ }
2641
+ return resolvedTypeNode.typeArguments.map((argumentNode) => {
2642
+ const argumentType = checker.getTypeFromTypeNode(argumentNode);
2643
+ return {
2644
+ tsType: argumentType,
2645
+ typeNode: resolveTypeNode(
2646
+ argumentType,
2647
+ checker,
2648
+ file,
2649
+ typeRegistry,
2650
+ visiting,
2651
+ argumentNode,
2652
+ extensionRegistry,
2653
+ diagnostics
2654
+ )
2655
+ };
2656
+ });
2657
+ }
2658
+ function applyDiscriminatorToObjectProperties(properties, node, subjectType, checker, file, diagnostics) {
2659
+ const directive = validateDiscriminatorDirective(node, checker, file, diagnostics);
2660
+ if (directive === null) {
2661
+ return properties;
2662
+ }
2663
+ const discriminatorValue = resolveDiscriminatorValue(
2664
+ getConcreteTypeArgumentForDiscriminator(
2665
+ node,
2666
+ subjectType,
2667
+ checker,
2668
+ directive.typeParameterName
2669
+ ),
2670
+ checker,
2671
+ directive.provenance,
2672
+ diagnostics
2673
+ );
2674
+ if (discriminatorValue === null) {
2675
+ return properties;
2676
+ }
2677
+ return properties.map(
2678
+ (property) => property.name === directive.fieldName ? {
2679
+ ...property,
2680
+ type: {
2681
+ kind: "enum",
2682
+ members: [{ value: discriminatorValue }]
2683
+ }
2684
+ } : property
2685
+ );
2686
+ }
2270
2687
  function analyzeFieldToIR(prop, checker, file, typeRegistry, visiting, diagnostics, hostType, extensionRegistry) {
2271
2688
  if (!ts3.isIdentifier(prop.name)) {
2272
2689
  return null;
@@ -2555,6 +2972,7 @@ function resolveTypeNode(type, checker, file, typeRegistry, visiting, sourceNode
2555
2972
  file,
2556
2973
  typeRegistry,
2557
2974
  visiting,
2975
+ sourceNode,
2558
2976
  extensionRegistry,
2559
2977
  diagnostics
2560
2978
  );
@@ -2818,35 +3236,60 @@ function typeNodeContainsReference(type, targetName) {
2818
3236
  }
2819
3237
  }
2820
3238
  }
2821
- function resolveObjectType(type, checker, file, typeRegistry, visiting, extensionRegistry, diagnostics) {
3239
+ function resolveObjectType(type, checker, file, typeRegistry, visiting, sourceNode, extensionRegistry, diagnostics) {
3240
+ const collectedDiagnostics = diagnostics ?? [];
2822
3241
  const typeName = getNamedTypeName(type);
2823
3242
  const namedTypeName = typeName ?? void 0;
2824
3243
  const namedDecl = getNamedTypeDeclaration(type);
2825
- const shouldRegisterNamedType = namedTypeName !== void 0 && !(namedTypeName === "Record" && namedDecl?.getSourceFile().fileName !== file);
3244
+ const referenceTypeArguments = extractReferenceTypeArguments(
3245
+ type,
3246
+ checker,
3247
+ file,
3248
+ typeRegistry,
3249
+ visiting,
3250
+ sourceNode,
3251
+ extensionRegistry,
3252
+ collectedDiagnostics
3253
+ );
3254
+ const instantiatedTypeName = namedTypeName !== void 0 && referenceTypeArguments.length > 0 ? buildInstantiatedReferenceName(
3255
+ namedTypeName,
3256
+ referenceTypeArguments.map((argument) => argument.tsType),
3257
+ checker
3258
+ ) : void 0;
3259
+ const registryTypeName = instantiatedTypeName ?? namedTypeName;
3260
+ const shouldRegisterNamedType = registryTypeName !== void 0 && !(registryTypeName === "Record" && namedDecl?.getSourceFile().fileName !== file);
2826
3261
  const clearNamedTypeRegistration = () => {
2827
- if (namedTypeName === void 0 || !shouldRegisterNamedType) {
3262
+ if (registryTypeName === void 0 || !shouldRegisterNamedType) {
2828
3263
  return;
2829
3264
  }
2830
- Reflect.deleteProperty(typeRegistry, namedTypeName);
3265
+ Reflect.deleteProperty(typeRegistry, registryTypeName);
2831
3266
  };
2832
3267
  if (visiting.has(type)) {
2833
- if (namedTypeName !== void 0 && shouldRegisterNamedType) {
2834
- return { kind: "reference", name: namedTypeName, typeArguments: [] };
3268
+ if (registryTypeName !== void 0 && shouldRegisterNamedType) {
3269
+ return {
3270
+ kind: "reference",
3271
+ name: registryTypeName,
3272
+ typeArguments: referenceTypeArguments.map((argument) => argument.typeNode)
3273
+ };
2835
3274
  }
2836
3275
  return { kind: "object", properties: [], additionalProperties: false };
2837
3276
  }
2838
- if (namedTypeName !== void 0 && shouldRegisterNamedType && !typeRegistry[namedTypeName]) {
2839
- typeRegistry[namedTypeName] = {
2840
- name: namedTypeName,
3277
+ if (registryTypeName !== void 0 && shouldRegisterNamedType && !typeRegistry[registryTypeName]) {
3278
+ typeRegistry[registryTypeName] = {
3279
+ name: registryTypeName,
2841
3280
  type: RESOLVING_TYPE_PLACEHOLDER,
2842
3281
  provenance: provenanceForDeclaration(namedDecl, file)
2843
3282
  };
2844
3283
  }
2845
3284
  visiting.add(type);
2846
- if (namedTypeName !== void 0 && shouldRegisterNamedType && typeRegistry[namedTypeName]?.type !== void 0) {
2847
- if (typeRegistry[namedTypeName].type !== RESOLVING_TYPE_PLACEHOLDER) {
3285
+ if (registryTypeName !== void 0 && shouldRegisterNamedType && typeRegistry[registryTypeName]?.type !== void 0) {
3286
+ if (typeRegistry[registryTypeName].type !== RESOLVING_TYPE_PLACEHOLDER) {
2848
3287
  visiting.delete(type);
2849
- return { kind: "reference", name: namedTypeName, typeArguments: [] };
3288
+ return {
3289
+ kind: "reference",
3290
+ name: registryTypeName,
3291
+ typeArguments: referenceTypeArguments.map((argument) => argument.typeNode)
3292
+ };
2850
3293
  }
2851
3294
  }
2852
3295
  const recordNode = tryResolveRecordType(
@@ -2856,24 +3299,28 @@ function resolveObjectType(type, checker, file, typeRegistry, visiting, extensio
2856
3299
  typeRegistry,
2857
3300
  visiting,
2858
3301
  extensionRegistry,
2859
- diagnostics
3302
+ collectedDiagnostics
2860
3303
  );
2861
3304
  if (recordNode) {
2862
3305
  visiting.delete(type);
2863
- if (namedTypeName !== void 0 && shouldRegisterNamedType) {
2864
- const isRecursiveRecord = typeNodeContainsReference(recordNode.valueType, namedTypeName);
3306
+ if (registryTypeName !== void 0 && shouldRegisterNamedType) {
3307
+ const isRecursiveRecord = typeNodeContainsReference(recordNode.valueType, registryTypeName);
2865
3308
  if (!isRecursiveRecord) {
2866
3309
  clearNamedTypeRegistration();
2867
3310
  return recordNode;
2868
3311
  }
2869
3312
  const annotations = namedDecl ? extractJSDocAnnotationNodes(namedDecl, file, makeParseOptions(extensionRegistry)) : void 0;
2870
- typeRegistry[namedTypeName] = {
2871
- name: namedTypeName,
3313
+ typeRegistry[registryTypeName] = {
3314
+ name: registryTypeName,
2872
3315
  type: recordNode,
2873
3316
  ...annotations !== void 0 && annotations.length > 0 && { annotations },
2874
3317
  provenance: provenanceForDeclaration(namedDecl, file)
2875
3318
  };
2876
- return { kind: "reference", name: namedTypeName, typeArguments: [] };
3319
+ return {
3320
+ kind: "reference",
3321
+ name: registryTypeName,
3322
+ typeArguments: referenceTypeArguments.map((argument) => argument.typeNode)
3323
+ };
2877
3324
  }
2878
3325
  return recordNode;
2879
3326
  }
@@ -2884,7 +3331,7 @@ function resolveObjectType(type, checker, file, typeRegistry, visiting, extensio
2884
3331
  file,
2885
3332
  typeRegistry,
2886
3333
  visiting,
2887
- diagnostics ?? [],
3334
+ collectedDiagnostics,
2888
3335
  extensionRegistry
2889
3336
  );
2890
3337
  for (const prop of type.getProperties()) {
@@ -2900,7 +3347,7 @@ function resolveObjectType(type, checker, file, typeRegistry, visiting, extensio
2900
3347
  visiting,
2901
3348
  declaration,
2902
3349
  extensionRegistry,
2903
- diagnostics
3350
+ collectedDiagnostics
2904
3351
  );
2905
3352
  const fieldNodeInfo = fieldInfoMap?.get(prop.name);
2906
3353
  properties.push({
@@ -2915,18 +3362,29 @@ function resolveObjectType(type, checker, file, typeRegistry, visiting, extensio
2915
3362
  visiting.delete(type);
2916
3363
  const objectNode = {
2917
3364
  kind: "object",
2918
- properties,
3365
+ properties: namedDecl !== void 0 && (ts3.isClassDeclaration(namedDecl) || ts3.isInterfaceDeclaration(namedDecl) || ts3.isTypeAliasDeclaration(namedDecl)) ? applyDiscriminatorToObjectProperties(
3366
+ properties,
3367
+ namedDecl,
3368
+ type,
3369
+ checker,
3370
+ file,
3371
+ collectedDiagnostics
3372
+ ) : properties,
2919
3373
  additionalProperties: true
2920
3374
  };
2921
- if (namedTypeName !== void 0 && shouldRegisterNamedType) {
3375
+ if (registryTypeName !== void 0 && shouldRegisterNamedType) {
2922
3376
  const annotations = namedDecl ? extractJSDocAnnotationNodes(namedDecl, file, makeParseOptions(extensionRegistry)) : void 0;
2923
- typeRegistry[namedTypeName] = {
2924
- name: namedTypeName,
3377
+ typeRegistry[registryTypeName] = {
3378
+ name: registryTypeName,
2925
3379
  type: objectNode,
2926
3380
  ...annotations !== void 0 && annotations.length > 0 && { annotations },
2927
3381
  provenance: provenanceForDeclaration(namedDecl, file)
2928
3382
  };
2929
- return { kind: "reference", name: namedTypeName, typeArguments: [] };
3383
+ return {
3384
+ kind: "reference",
3385
+ name: registryTypeName,
3386
+ typeArguments: referenceTypeArguments.map((argument) => argument.typeNode)
3387
+ };
2930
3388
  }
2931
3389
  return objectNode;
2932
3390
  }