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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. package/dist/__tests__/fixtures/class-schema-regressions.d.ts +4 -0
  2. package/dist/__tests__/fixtures/class-schema-regressions.d.ts.map +1 -1
  3. package/dist/__tests__/parity/utils.d.ts.map +1 -1
  4. package/dist/analyzer/class-analyzer.d.ts +4 -1
  5. package/dist/analyzer/class-analyzer.d.ts.map +1 -1
  6. package/dist/analyzer/jsdoc-constraints.d.ts +2 -1
  7. package/dist/analyzer/jsdoc-constraints.d.ts.map +1 -1
  8. package/dist/analyzer/tsdoc-parser.d.ts +13 -3
  9. package/dist/analyzer/tsdoc-parser.d.ts.map +1 -1
  10. package/dist/browser.cjs +30 -748
  11. package/dist/browser.cjs.map +1 -1
  12. package/dist/browser.js +32 -748
  13. package/dist/browser.js.map +1 -1
  14. package/dist/build.d.ts +14 -14
  15. package/dist/cli.cjs +691 -1101
  16. package/dist/cli.cjs.map +1 -1
  17. package/dist/cli.js +704 -1100
  18. package/dist/cli.js.map +1 -1
  19. package/dist/generators/class-schema.d.ts.map +1 -1
  20. package/dist/index.cjs +689 -1095
  21. package/dist/index.cjs.map +1 -1
  22. package/dist/index.js +703 -1095
  23. package/dist/index.js.map +1 -1
  24. package/dist/internals.cjs +663 -1069
  25. package/dist/internals.cjs.map +1 -1
  26. package/dist/internals.js +675 -1067
  27. package/dist/internals.js.map +1 -1
  28. package/dist/ui-schema/schema.d.ts +14 -14
  29. package/dist/validate/constraint-validator.d.ts +6 -45
  30. package/dist/validate/constraint-validator.d.ts.map +1 -1
  31. package/package.json +2 -1
  32. package/dist/__tests__/json-utils.test.d.ts +0 -5
  33. package/dist/__tests__/json-utils.test.d.ts.map +0 -1
  34. package/dist/analyzer/json-utils.d.ts +0 -22
  35. package/dist/analyzer/json-utils.d.ts.map +0 -1
package/dist/index.js CHANGED
@@ -1219,6 +1219,19 @@ import * as ts2 from "typescript";
1219
1219
 
1220
1220
  // src/analyzer/tsdoc-parser.ts
1221
1221
  import * as ts from "typescript";
1222
+ import {
1223
+ checkSyntheticTagApplication,
1224
+ extractPathTarget as extractSharedPathTarget,
1225
+ getTagDefinition,
1226
+ hasTypeSemanticCapability,
1227
+ parseConstraintTagValue,
1228
+ parseDefaultValueTagValue,
1229
+ resolveDeclarationPlacement,
1230
+ resolvePathTargetType,
1231
+ sliceCommentSpan,
1232
+ parseCommentBlock,
1233
+ parseTagSyntax
1234
+ } from "@formspec/analysis";
1222
1235
  import {
1223
1236
  TSDocParser,
1224
1237
  TSDocConfiguration,
@@ -1233,30 +1246,6 @@ import {
1233
1246
  normalizeConstraintTagName,
1234
1247
  isBuiltinConstraintName
1235
1248
  } from "@formspec/core";
1236
-
1237
- // src/analyzer/json-utils.ts
1238
- function tryParseJson(text) {
1239
- try {
1240
- return JSON.parse(text);
1241
- } catch {
1242
- return null;
1243
- }
1244
- }
1245
-
1246
- // src/analyzer/tsdoc-parser.ts
1247
- var NUMERIC_CONSTRAINT_MAP = {
1248
- minimum: "minimum",
1249
- maximum: "maximum",
1250
- exclusiveMinimum: "exclusiveMinimum",
1251
- exclusiveMaximum: "exclusiveMaximum",
1252
- multipleOf: "multipleOf"
1253
- };
1254
- var LENGTH_CONSTRAINT_MAP = {
1255
- minLength: "minLength",
1256
- maxLength: "maxLength",
1257
- minItems: "minItems",
1258
- maxItems: "maxItems"
1259
- };
1260
1249
  var TAGS_REQUIRING_RAW_TEXT = /* @__PURE__ */ new Set(["pattern", "enumOptions", "defaultValue"]);
1261
1250
  function createFormSpecTSDocConfig(extensionTagNames = []) {
1262
1251
  const config = new TSDocConfiguration();
@@ -1289,7 +1278,294 @@ function createFormSpecTSDocConfig(extensionTagNames = []) {
1289
1278
  }
1290
1279
  return config;
1291
1280
  }
1281
+ function sharedCommentSyntaxOptions(options, offset) {
1282
+ const extensions = options?.extensionRegistry?.extensions;
1283
+ return {
1284
+ ...offset !== void 0 ? { offset } : {},
1285
+ ...extensions !== void 0 ? { extensions } : {}
1286
+ };
1287
+ }
1288
+ function sharedTagValueOptions(options) {
1289
+ return {
1290
+ ...options?.extensionRegistry !== void 0 ? { registry: options.extensionRegistry } : {},
1291
+ ...options?.fieldType !== void 0 ? { fieldType: options.fieldType } : {}
1292
+ };
1293
+ }
1294
+ var SYNTHETIC_TYPE_FORMAT_FLAGS = ts.TypeFormatFlags.NoTruncation | ts.TypeFormatFlags.UseAliasDefinedOutsideCurrentScope;
1295
+ function buildSupportingDeclarations(sourceFile) {
1296
+ return sourceFile.statements.filter(
1297
+ (statement) => !ts.isImportDeclaration(statement) && !ts.isImportEqualsDeclaration(statement) && !(ts.isExportDeclaration(statement) && statement.moduleSpecifier !== void 0)
1298
+ ).map((statement) => statement.getText(sourceFile));
1299
+ }
1300
+ function renderSyntheticArgumentExpression(valueKind, argumentText) {
1301
+ const trimmed = argumentText.trim();
1302
+ if (trimmed === "") {
1303
+ return null;
1304
+ }
1305
+ switch (valueKind) {
1306
+ case "number":
1307
+ case "integer":
1308
+ case "signedInteger":
1309
+ return Number.isFinite(Number(trimmed)) ? trimmed : JSON.stringify(trimmed);
1310
+ case "string":
1311
+ return JSON.stringify(argumentText);
1312
+ case "json":
1313
+ try {
1314
+ JSON.parse(trimmed);
1315
+ return `(${trimmed})`;
1316
+ } catch {
1317
+ return JSON.stringify(trimmed);
1318
+ }
1319
+ case "boolean":
1320
+ return trimmed === "true" || trimmed === "false" ? trimmed : JSON.stringify(trimmed);
1321
+ case "condition":
1322
+ return "undefined as unknown as FormSpecCondition";
1323
+ case null:
1324
+ return null;
1325
+ default: {
1326
+ return String(valueKind);
1327
+ }
1328
+ }
1329
+ }
1330
+ function getArrayElementType(type, checker) {
1331
+ if (!checker.isArrayType(type)) {
1332
+ return null;
1333
+ }
1334
+ return checker.getTypeArguments(type)[0] ?? null;
1335
+ }
1336
+ function supportsConstraintCapability(type, checker, capability) {
1337
+ if (capability === void 0) {
1338
+ return true;
1339
+ }
1340
+ if (hasTypeSemanticCapability(type, checker, capability)) {
1341
+ return true;
1342
+ }
1343
+ if (capability === "string-like") {
1344
+ const itemType = getArrayElementType(type, checker);
1345
+ return itemType !== null && hasTypeSemanticCapability(itemType, checker, capability);
1346
+ }
1347
+ return false;
1348
+ }
1349
+ function makeDiagnostic(code, message, provenance) {
1350
+ return {
1351
+ code,
1352
+ message,
1353
+ severity: "error",
1354
+ primaryLocation: provenance,
1355
+ relatedLocations: []
1356
+ };
1357
+ }
1358
+ function placementLabel(placement) {
1359
+ switch (placement) {
1360
+ case "class":
1361
+ return "class declarations";
1362
+ case "class-field":
1363
+ return "class fields";
1364
+ case "class-method":
1365
+ return "class methods";
1366
+ case "interface":
1367
+ return "interface declarations";
1368
+ case "interface-field":
1369
+ return "interface fields";
1370
+ case "type-alias":
1371
+ return "type aliases";
1372
+ case "type-alias-field":
1373
+ return "type-alias properties";
1374
+ case "variable":
1375
+ return "variables";
1376
+ case "function":
1377
+ return "functions";
1378
+ case "function-parameter":
1379
+ return "function parameters";
1380
+ case "method-parameter":
1381
+ return "method parameters";
1382
+ default: {
1383
+ const exhaustive = placement;
1384
+ return String(exhaustive);
1385
+ }
1386
+ }
1387
+ }
1388
+ function capabilityLabel(capability) {
1389
+ switch (capability) {
1390
+ case "numeric-comparable":
1391
+ return "number";
1392
+ case "string-like":
1393
+ return "string";
1394
+ case "array-like":
1395
+ return "array";
1396
+ case "enum-member-addressable":
1397
+ return "enum";
1398
+ case "json-like":
1399
+ return "JSON-compatible";
1400
+ case "object-like":
1401
+ return "object";
1402
+ case "condition-like":
1403
+ return "conditional";
1404
+ case void 0:
1405
+ return "compatible";
1406
+ default:
1407
+ return capability;
1408
+ }
1409
+ }
1410
+ function getBroadenedCustomTypeId(fieldType) {
1411
+ if (fieldType?.kind === "custom") {
1412
+ return fieldType.typeId;
1413
+ }
1414
+ if (fieldType?.kind !== "union") {
1415
+ return void 0;
1416
+ }
1417
+ const customMembers = fieldType.members.filter(
1418
+ (member) => member.kind === "custom"
1419
+ );
1420
+ if (customMembers.length !== 1) {
1421
+ return void 0;
1422
+ }
1423
+ const nonCustomMembers = fieldType.members.filter((member) => member.kind !== "custom");
1424
+ const allOtherMembersAreNull = nonCustomMembers.every(
1425
+ (member) => member.kind === "primitive" && member.primitiveKind === "null"
1426
+ );
1427
+ const customMember = customMembers[0];
1428
+ return allOtherMembersAreNull && customMember !== void 0 ? customMember.typeId : void 0;
1429
+ }
1430
+ function hasBuiltinConstraintBroadening(tagName, options) {
1431
+ const broadenedTypeId = getBroadenedCustomTypeId(options?.fieldType);
1432
+ return broadenedTypeId !== void 0 && options?.extensionRegistry?.findBuiltinConstraintBroadening(broadenedTypeId, tagName) !== void 0;
1433
+ }
1434
+ function buildCompilerBackedConstraintDiagnostics(node, sourceFile, tagName, parsedTag, provenance, supportingDeclarations, options) {
1435
+ if (!isBuiltinConstraintName(tagName)) {
1436
+ return [];
1437
+ }
1438
+ const checker = options?.checker;
1439
+ const subjectType = options?.subjectType;
1440
+ if (checker === void 0 || subjectType === void 0) {
1441
+ return [];
1442
+ }
1443
+ const placement = resolveDeclarationPlacement(node);
1444
+ if (placement === null) {
1445
+ return [];
1446
+ }
1447
+ const definition = getTagDefinition(tagName, options?.extensionRegistry?.extensions);
1448
+ if (definition === null) {
1449
+ return [];
1450
+ }
1451
+ if (!definition.placements.includes(placement)) {
1452
+ return [
1453
+ makeDiagnostic(
1454
+ "INVALID_TAG_PLACEMENT",
1455
+ `Tag "@${tagName}" is not allowed on ${placementLabel(placement)}.`,
1456
+ provenance
1457
+ )
1458
+ ];
1459
+ }
1460
+ const target = parsedTag?.target ?? null;
1461
+ const hasBroadening = target === null && hasBuiltinConstraintBroadening(tagName, options);
1462
+ if (target !== null) {
1463
+ if (target.kind !== "path") {
1464
+ return [
1465
+ makeDiagnostic(
1466
+ "UNSUPPORTED_TARGETING_SYNTAX",
1467
+ `Tag "@${tagName}" does not support ${target.kind} targeting syntax.`,
1468
+ provenance
1469
+ )
1470
+ ];
1471
+ }
1472
+ if (!target.valid || target.path === null) {
1473
+ return [
1474
+ makeDiagnostic(
1475
+ "UNSUPPORTED_TARGETING_SYNTAX",
1476
+ `Tag "@${tagName}" has invalid path targeting syntax.`,
1477
+ provenance
1478
+ )
1479
+ ];
1480
+ }
1481
+ const resolution = resolvePathTargetType(subjectType, checker, target.path.segments);
1482
+ if (resolution.kind === "missing-property") {
1483
+ return [
1484
+ makeDiagnostic(
1485
+ "UNKNOWN_PATH_TARGET",
1486
+ `Target "${target.rawText}": path-targeted constraint "${tagName}" references unknown path segment "${resolution.segment}"`,
1487
+ provenance
1488
+ )
1489
+ ];
1490
+ }
1491
+ if (resolution.kind === "unresolvable") {
1492
+ const actualType = checker.typeToString(resolution.type, node, SYNTHETIC_TYPE_FORMAT_FLAGS);
1493
+ return [
1494
+ makeDiagnostic(
1495
+ "TYPE_MISMATCH",
1496
+ `Target "${target.rawText}": path-targeted constraint "${tagName}" is invalid because type "${actualType}" cannot be traversed`,
1497
+ provenance
1498
+ )
1499
+ ];
1500
+ }
1501
+ const requiredCapability = definition.capabilities[0];
1502
+ if (requiredCapability !== void 0 && !supportsConstraintCapability(resolution.type, checker, requiredCapability)) {
1503
+ const actualType = checker.typeToString(resolution.type, node, SYNTHETIC_TYPE_FORMAT_FLAGS);
1504
+ return [
1505
+ makeDiagnostic(
1506
+ "TYPE_MISMATCH",
1507
+ `Target "${target.rawText}": constraint "${tagName}" is only valid on ${capabilityLabel(requiredCapability)} targets, but field type is "${actualType}"`,
1508
+ provenance
1509
+ )
1510
+ ];
1511
+ }
1512
+ } else if (!hasBroadening) {
1513
+ const requiredCapability = definition.capabilities[0];
1514
+ if (requiredCapability !== void 0 && !supportsConstraintCapability(subjectType, checker, requiredCapability)) {
1515
+ const actualType = checker.typeToString(subjectType, node, SYNTHETIC_TYPE_FORMAT_FLAGS);
1516
+ return [
1517
+ makeDiagnostic(
1518
+ "TYPE_MISMATCH",
1519
+ `Target "${node.getText(sourceFile)}": constraint "${tagName}" is only valid on ${capabilityLabel(requiredCapability)} targets, but field type is "${actualType}"`,
1520
+ provenance
1521
+ )
1522
+ ];
1523
+ }
1524
+ }
1525
+ const argumentExpression = renderSyntheticArgumentExpression(
1526
+ definition.valueKind,
1527
+ parsedTag?.argumentText ?? ""
1528
+ );
1529
+ if (definition.requiresArgument && argumentExpression === null) {
1530
+ return [];
1531
+ }
1532
+ if (hasBroadening) {
1533
+ return [];
1534
+ }
1535
+ const subjectTypeText = checker.typeToString(subjectType, node, SYNTHETIC_TYPE_FORMAT_FLAGS);
1536
+ const hostType = options?.hostType ?? subjectType;
1537
+ const hostTypeText = checker.typeToString(hostType, node, SYNTHETIC_TYPE_FORMAT_FLAGS);
1538
+ const result = checkSyntheticTagApplication({
1539
+ tagName,
1540
+ placement,
1541
+ hostType: hostTypeText,
1542
+ subjectType: subjectTypeText,
1543
+ ...target?.kind === "path" ? { target: { kind: "path", text: target.rawText } } : {},
1544
+ ...argumentExpression !== null ? { argumentExpression } : {},
1545
+ supportingDeclarations,
1546
+ ...options?.extensionRegistry !== void 0 ? {
1547
+ extensions: options.extensionRegistry.extensions.map((extension) => ({
1548
+ extensionId: extension.extensionId,
1549
+ ...extension.constraintTags !== void 0 ? {
1550
+ constraintTags: extension.constraintTags.map((tag) => ({ tagName: tag.tagName }))
1551
+ } : {}
1552
+ }))
1553
+ } : {}
1554
+ });
1555
+ if (result.diagnostics.length === 0) {
1556
+ return [];
1557
+ }
1558
+ const expectedLabel = definition.valueKind === null ? "compatible argument" : capabilityLabel(definition.valueKind);
1559
+ return [
1560
+ makeDiagnostic(
1561
+ "TYPE_MISMATCH",
1562
+ `Tag "@${tagName}" received an invalid argument for ${expectedLabel}.`,
1563
+ provenance
1564
+ )
1565
+ ];
1566
+ }
1292
1567
  var parserCache = /* @__PURE__ */ new Map();
1568
+ var parseResultCache = /* @__PURE__ */ new Map();
1293
1569
  function getParser(options) {
1294
1570
  const extensionTagNames = [
1295
1571
  ...options?.extensionRegistry?.extensions.flatMap(
@@ -1305,18 +1581,54 @@ function getParser(options) {
1305
1581
  parserCache.set(cacheKey, parser);
1306
1582
  return parser;
1307
1583
  }
1584
+ function getExtensionRegistryCacheKey(registry) {
1585
+ if (registry === void 0) {
1586
+ return "";
1587
+ }
1588
+ return registry.extensions.map(
1589
+ (extension) => JSON.stringify({
1590
+ extensionId: extension.extensionId,
1591
+ typeNames: extension.types?.map((type) => type.typeName) ?? [],
1592
+ constraintTags: extension.constraintTags?.map((tag) => tag.tagName) ?? []
1593
+ })
1594
+ ).join("|");
1595
+ }
1596
+ function getParseCacheKey(node, file, options) {
1597
+ const sourceFile = node.getSourceFile();
1598
+ const checker = options?.checker;
1599
+ return JSON.stringify({
1600
+ file,
1601
+ sourceFile: sourceFile.fileName,
1602
+ sourceText: sourceFile.text,
1603
+ start: node.getFullStart(),
1604
+ end: node.getEnd(),
1605
+ fieldType: options?.fieldType ?? null,
1606
+ subjectType: checker !== void 0 && options?.subjectType !== void 0 ? checker.typeToString(options.subjectType, node, SYNTHETIC_TYPE_FORMAT_FLAGS) : null,
1607
+ hostType: checker !== void 0 && options?.hostType !== void 0 ? checker.typeToString(options.hostType, node, SYNTHETIC_TYPE_FORMAT_FLAGS) : null,
1608
+ extensions: getExtensionRegistryCacheKey(options?.extensionRegistry)
1609
+ });
1610
+ }
1308
1611
  function parseTSDocTags(node, file = "", options) {
1612
+ const cacheKey = getParseCacheKey(node, file, options);
1613
+ const cached = parseResultCache.get(cacheKey);
1614
+ if (cached !== void 0) {
1615
+ return cached;
1616
+ }
1309
1617
  const constraints = [];
1310
1618
  const annotations = [];
1619
+ const diagnostics = [];
1311
1620
  let displayName;
1312
1621
  let description;
1313
1622
  let placeholder;
1314
1623
  let displayNameProvenance;
1315
1624
  let descriptionProvenance;
1316
1625
  let placeholderProvenance;
1626
+ const rawTextTags = [];
1317
1627
  const sourceFile = node.getSourceFile();
1318
1628
  const sourceText = sourceFile.getFullText();
1629
+ const supportingDeclarations = buildSupportingDeclarations(sourceFile);
1319
1630
  const commentRanges = ts.getLeadingCommentRanges(sourceText, node.getFullStart());
1631
+ const rawTextFallbacks = collectRawTextFallbacks(node, file);
1320
1632
  if (commentRanges) {
1321
1633
  for (const range of commentRanges) {
1322
1634
  if (range.kind !== ts.SyntaxKind.MultiLineCommentTrivia) {
@@ -1331,12 +1643,33 @@ function parseTSDocTags(node, file = "", options) {
1331
1643
  TextRange.fromStringRange(sourceText, range.pos, range.end)
1332
1644
  );
1333
1645
  const docComment = parserContext.docComment;
1646
+ const parsedComment = parseCommentBlock(
1647
+ commentText,
1648
+ sharedCommentSyntaxOptions(options, range.pos)
1649
+ );
1650
+ let parsedTagCursor = 0;
1651
+ const nextParsedTag = (normalizedTagName) => {
1652
+ while (parsedTagCursor < parsedComment.tags.length) {
1653
+ const candidate = parsedComment.tags[parsedTagCursor];
1654
+ parsedTagCursor += 1;
1655
+ if (candidate?.normalizedTagName === normalizedTagName) {
1656
+ return candidate;
1657
+ }
1658
+ }
1659
+ return null;
1660
+ };
1661
+ for (const parsedTag of parsedComment.tags) {
1662
+ if (TAGS_REQUIRING_RAW_TEXT.has(parsedTag.normalizedTagName)) {
1663
+ rawTextTags.push({ tag: parsedTag, commentText, commentOffset: range.pos });
1664
+ }
1665
+ }
1334
1666
  for (const block of docComment.customBlocks) {
1335
1667
  const tagName = normalizeConstraintTagName(block.blockTag.tagName.substring(1));
1668
+ const parsedTag = nextParsedTag(tagName);
1336
1669
  if (tagName === "displayName" || tagName === "description" || tagName === "format" || tagName === "placeholder") {
1337
- const text2 = extractBlockText(block).trim();
1670
+ const text2 = getBestBlockPayloadText(parsedTag, commentText, range.pos, block);
1338
1671
  if (text2 === "") continue;
1339
- const provenance2 = provenanceForComment(range, sourceFile, file, tagName);
1672
+ const provenance2 = parsedTag !== null ? provenanceForParsedTag(parsedTag, sourceFile, file) : provenanceForComment(range, sourceFile, file, tagName);
1340
1673
  switch (tagName) {
1341
1674
  case "displayName":
1342
1675
  if (!isMemberTargetDisplayName(text2) && displayName === void 0) {
@@ -1366,11 +1699,29 @@ function parseTSDocTags(node, file = "", options) {
1366
1699
  continue;
1367
1700
  }
1368
1701
  if (TAGS_REQUIRING_RAW_TEXT.has(tagName)) continue;
1369
- const text = extractBlockText(block).trim();
1702
+ const text = getBestBlockPayloadText(parsedTag, commentText, range.pos, block);
1370
1703
  const expectedType = isBuiltinConstraintName(tagName) ? BUILTIN_CONSTRAINT_DEFINITIONS[tagName] : void 0;
1371
1704
  if (text === "" && expectedType !== "boolean") continue;
1372
- const provenance = provenanceForComment(range, sourceFile, file, tagName);
1373
- const constraintNode = parseConstraintValue(tagName, text, provenance, options);
1705
+ const provenance = parsedTag !== null ? provenanceForParsedTag(parsedTag, sourceFile, file) : provenanceForComment(range, sourceFile, file, tagName);
1706
+ const compilerDiagnostics = buildCompilerBackedConstraintDiagnostics(
1707
+ node,
1708
+ sourceFile,
1709
+ tagName,
1710
+ parsedTag,
1711
+ provenance,
1712
+ supportingDeclarations,
1713
+ options
1714
+ );
1715
+ if (compilerDiagnostics.length > 0) {
1716
+ diagnostics.push(...compilerDiagnostics);
1717
+ continue;
1718
+ }
1719
+ const constraintNode = parseConstraintTagValue(
1720
+ tagName,
1721
+ text,
1722
+ provenance,
1723
+ sharedTagValueOptions(options)
1724
+ );
1374
1725
  if (constraintNode) {
1375
1726
  constraints.push(constraintNode);
1376
1727
  }
@@ -1424,57 +1775,114 @@ function parseTSDocTags(node, file = "", options) {
1424
1775
  provenance: placeholderProvenance
1425
1776
  });
1426
1777
  }
1427
- const jsDocTagsAll = ts.getJSDocTags(node);
1428
- for (const tag of jsDocTagsAll) {
1429
- const tagName = normalizeConstraintTagName(tag.tagName.text);
1430
- if (!TAGS_REQUIRING_RAW_TEXT.has(tagName)) continue;
1431
- const commentText = getTagCommentText(tag);
1432
- if (commentText === void 0 || commentText.trim() === "") continue;
1433
- const text = commentText.trim();
1434
- const provenance = provenanceForJSDocTag(tag, file);
1435
- if (tagName === "defaultValue") {
1436
- const defaultValueNode = parseDefaultValueValue(text, provenance);
1437
- annotations.push(defaultValueNode);
1438
- continue;
1778
+ if (rawTextTags.length > 0) {
1779
+ for (const rawTextTag of rawTextTags) {
1780
+ const fallbackQueue = rawTextFallbacks.get(rawTextTag.tag.normalizedTagName);
1781
+ const fallback = fallbackQueue?.shift();
1782
+ const text = choosePreferredPayloadText(
1783
+ getSharedPayloadText(rawTextTag.tag, rawTextTag.commentText, rawTextTag.commentOffset),
1784
+ fallback?.text ?? ""
1785
+ );
1786
+ if (text === "") continue;
1787
+ const provenance = provenanceForParsedTag(rawTextTag.tag, sourceFile, file);
1788
+ if (rawTextTag.tag.normalizedTagName === "defaultValue") {
1789
+ const defaultValueNode = parseDefaultValueTagValue(text, provenance);
1790
+ annotations.push(defaultValueNode);
1791
+ continue;
1792
+ }
1793
+ const compilerDiagnostics = buildCompilerBackedConstraintDiagnostics(
1794
+ node,
1795
+ sourceFile,
1796
+ rawTextTag.tag.normalizedTagName,
1797
+ rawTextTag.tag,
1798
+ provenance,
1799
+ supportingDeclarations,
1800
+ options
1801
+ );
1802
+ if (compilerDiagnostics.length > 0) {
1803
+ diagnostics.push(...compilerDiagnostics);
1804
+ continue;
1805
+ }
1806
+ const constraintNode = parseConstraintTagValue(
1807
+ rawTextTag.tag.normalizedTagName,
1808
+ text,
1809
+ provenance,
1810
+ sharedTagValueOptions(options)
1811
+ );
1812
+ if (constraintNode) {
1813
+ constraints.push(constraintNode);
1814
+ }
1439
1815
  }
1440
- const constraintNode = parseConstraintValue(tagName, text, provenance, options);
1441
- if (constraintNode) {
1442
- constraints.push(constraintNode);
1816
+ }
1817
+ for (const [tagName, fallbacks] of rawTextFallbacks) {
1818
+ for (const fallback of fallbacks) {
1819
+ const text = fallback.text.trim();
1820
+ if (text === "") continue;
1821
+ const provenance = fallback.provenance;
1822
+ if (tagName === "defaultValue") {
1823
+ const defaultValueNode = parseDefaultValueTagValue(text, provenance);
1824
+ annotations.push(defaultValueNode);
1825
+ continue;
1826
+ }
1827
+ const compilerDiagnostics = buildCompilerBackedConstraintDiagnostics(
1828
+ node,
1829
+ sourceFile,
1830
+ tagName,
1831
+ null,
1832
+ provenance,
1833
+ supportingDeclarations,
1834
+ options
1835
+ );
1836
+ if (compilerDiagnostics.length > 0) {
1837
+ diagnostics.push(...compilerDiagnostics);
1838
+ continue;
1839
+ }
1840
+ const constraintNode = parseConstraintTagValue(
1841
+ tagName,
1842
+ text,
1843
+ provenance,
1844
+ sharedTagValueOptions(options)
1845
+ );
1846
+ if (constraintNode) {
1847
+ constraints.push(constraintNode);
1848
+ }
1443
1849
  }
1444
1850
  }
1445
- return { constraints, annotations };
1851
+ const result = { constraints, annotations, diagnostics };
1852
+ parseResultCache.set(cacheKey, result);
1853
+ return result;
1446
1854
  }
1447
1855
  function extractDisplayNameMetadata(node) {
1448
1856
  let displayName;
1449
1857
  const memberDisplayNames = /* @__PURE__ */ new Map();
1450
- for (const tag of ts.getJSDocTags(node)) {
1451
- const tagName = normalizeConstraintTagName(tag.tagName.text);
1452
- if (tagName !== "displayName") continue;
1453
- const commentText = getTagCommentText(tag);
1454
- if (commentText === void 0) continue;
1455
- const text = commentText.trim();
1456
- if (text === "") continue;
1457
- const memberTarget = parseMemberTargetDisplayName(text);
1458
- if (memberTarget) {
1459
- memberDisplayNames.set(memberTarget.target, memberTarget.label);
1460
- continue;
1858
+ const sourceFile = node.getSourceFile();
1859
+ const sourceText = sourceFile.getFullText();
1860
+ const commentRanges = ts.getLeadingCommentRanges(sourceText, node.getFullStart());
1861
+ if (commentRanges) {
1862
+ for (const range of commentRanges) {
1863
+ if (range.kind !== ts.SyntaxKind.MultiLineCommentTrivia) continue;
1864
+ const commentText = sourceText.substring(range.pos, range.end);
1865
+ if (!commentText.startsWith("/**")) continue;
1866
+ const parsed = parseCommentBlock(commentText);
1867
+ for (const tag of parsed.tags) {
1868
+ if (tag.normalizedTagName !== "displayName") {
1869
+ continue;
1870
+ }
1871
+ if (tag.target !== null && tag.argumentText !== "") {
1872
+ memberDisplayNames.set(tag.target.rawText, tag.argumentText);
1873
+ continue;
1874
+ }
1875
+ if (tag.argumentText !== "") {
1876
+ displayName ??= tag.argumentText;
1877
+ }
1878
+ }
1461
1879
  }
1462
- displayName ??= text;
1463
1880
  }
1464
1881
  return {
1465
1882
  ...displayName !== void 0 && { displayName },
1466
1883
  memberDisplayNames
1467
1884
  };
1468
1885
  }
1469
- function extractPathTarget(text) {
1470
- const trimmed = text.trimStart();
1471
- const match = /^:([a-zA-Z_]\w*)(?:\s+([\s\S]*))?$/.exec(trimmed);
1472
- if (!match?.[1]) return null;
1473
- return {
1474
- path: { segments: [match[1]] },
1475
- remainingText: match[2] ?? ""
1476
- };
1477
- }
1478
1886
  function extractBlockText(block) {
1479
1887
  return extractPlainText(block.content);
1480
1888
  }
@@ -1493,258 +1901,100 @@ function extractPlainText(node) {
1493
1901
  }
1494
1902
  return result;
1495
1903
  }
1496
- function parseConstraintValue(tagName, text, provenance, options) {
1497
- const customConstraint = parseExtensionConstraintValue(tagName, text, provenance, options);
1498
- if (customConstraint) {
1499
- return customConstraint;
1500
- }
1501
- if (!isBuiltinConstraintName(tagName)) {
1502
- return null;
1904
+ function choosePreferredPayloadText(primary, fallback) {
1905
+ const preferred = primary.trim();
1906
+ const alternate = fallback.trim();
1907
+ if (preferred === "") return alternate;
1908
+ if (alternate === "") return preferred;
1909
+ if (alternate.includes("\n")) return alternate;
1910
+ if (alternate.length > preferred.length && alternate.startsWith(preferred)) {
1911
+ return alternate;
1503
1912
  }
1504
- const pathResult = extractPathTarget(text);
1505
- const effectiveText = pathResult ? pathResult.remainingText : text;
1506
- const path3 = pathResult?.path;
1507
- const expectedType = BUILTIN_CONSTRAINT_DEFINITIONS[tagName];
1508
- if (expectedType === "number") {
1509
- const value = Number(effectiveText);
1510
- if (Number.isNaN(value)) {
1511
- return null;
1512
- }
1513
- const numericKind = NUMERIC_CONSTRAINT_MAP[tagName];
1514
- if (numericKind) {
1515
- return {
1516
- kind: "constraint",
1517
- constraintKind: numericKind,
1518
- value,
1519
- ...path3 && { path: path3 },
1520
- provenance
1521
- };
1522
- }
1523
- const lengthKind = LENGTH_CONSTRAINT_MAP[tagName];
1524
- if (lengthKind) {
1525
- return {
1526
- kind: "constraint",
1527
- constraintKind: lengthKind,
1528
- value,
1529
- ...path3 && { path: path3 },
1530
- provenance
1531
- };
1532
- }
1533
- return null;
1534
- }
1535
- if (expectedType === "boolean") {
1536
- const trimmed = effectiveText.trim();
1537
- if (trimmed !== "" && trimmed !== "true") {
1538
- return null;
1539
- }
1540
- if (tagName === "uniqueItems") {
1541
- return {
1542
- kind: "constraint",
1543
- constraintKind: "uniqueItems",
1544
- value: true,
1545
- ...path3 && { path: path3 },
1546
- provenance
1547
- };
1548
- }
1549
- return null;
1913
+ return preferred;
1914
+ }
1915
+ function getSharedPayloadText(tag, commentText, commentOffset) {
1916
+ if (tag.payloadSpan === null) {
1917
+ return "";
1550
1918
  }
1551
- if (expectedType === "json") {
1552
- if (tagName === "const") {
1553
- const trimmedText = effectiveText.trim();
1554
- if (trimmedText === "") return null;
1555
- try {
1556
- const parsed2 = JSON.parse(trimmedText);
1557
- return {
1558
- kind: "constraint",
1559
- constraintKind: "const",
1560
- value: parsed2,
1561
- ...path3 && { path: path3 },
1562
- provenance
1563
- };
1564
- } catch {
1565
- return {
1566
- kind: "constraint",
1567
- constraintKind: "const",
1568
- value: trimmedText,
1569
- ...path3 && { path: path3 },
1570
- provenance
1571
- };
1572
- }
1573
- }
1574
- const parsed = tryParseJson(effectiveText);
1575
- if (!Array.isArray(parsed)) {
1576
- return null;
1577
- }
1578
- const members = [];
1579
- for (const item of parsed) {
1580
- if (typeof item === "string" || typeof item === "number") {
1581
- members.push(item);
1582
- } else if (typeof item === "object" && item !== null && "id" in item) {
1583
- const id = item["id"];
1584
- if (typeof id === "string" || typeof id === "number") {
1585
- members.push(id);
1586
- }
1587
- }
1588
- }
1589
- return {
1590
- kind: "constraint",
1591
- constraintKind: "allowedMembers",
1592
- members,
1593
- ...path3 && { path: path3 },
1594
- provenance
1595
- };
1919
+ return sliceCommentSpan(commentText, tag.payloadSpan, {
1920
+ offset: commentOffset
1921
+ }).trim();
1922
+ }
1923
+ function getBestBlockPayloadText(tag, commentText, commentOffset, block) {
1924
+ const sharedText = tag === null ? "" : getSharedPayloadText(tag, commentText, commentOffset);
1925
+ const blockText = extractBlockText(block).replace(/\s+/g, " ").trim();
1926
+ return choosePreferredPayloadText(sharedText, blockText);
1927
+ }
1928
+ function collectRawTextFallbacks(node, file) {
1929
+ const fallbacks = /* @__PURE__ */ new Map();
1930
+ for (const tag of ts.getJSDocTags(node)) {
1931
+ const tagName = normalizeConstraintTagName(tag.tagName.text);
1932
+ if (!TAGS_REQUIRING_RAW_TEXT.has(tagName)) continue;
1933
+ const commentText = getTagCommentText(tag)?.trim() ?? "";
1934
+ if (commentText === "") continue;
1935
+ const entries = fallbacks.get(tagName) ?? [];
1936
+ entries.push({
1937
+ text: commentText,
1938
+ provenance: provenanceForJSDocTag(tag, file)
1939
+ });
1940
+ fallbacks.set(tagName, entries);
1596
1941
  }
1942
+ return fallbacks;
1943
+ }
1944
+ function isMemberTargetDisplayName(text) {
1945
+ return parseTagSyntax("displayName", text).target !== null;
1946
+ }
1947
+ function provenanceForComment(range, sourceFile, file, tagName) {
1948
+ const { line, character } = sourceFile.getLineAndCharacterOfPosition(range.pos);
1597
1949
  return {
1598
- kind: "constraint",
1599
- constraintKind: "pattern",
1600
- pattern: effectiveText,
1601
- ...path3 && { path: path3 },
1602
- provenance
1950
+ surface: "tsdoc",
1951
+ file,
1952
+ line: line + 1,
1953
+ column: character,
1954
+ tagName: "@" + tagName
1603
1955
  };
1604
1956
  }
1605
- function parseExtensionConstraintValue(tagName, text, provenance, options) {
1606
- const pathResult = extractPathTarget(text);
1607
- const effectiveText = pathResult ? pathResult.remainingText : text;
1608
- const path3 = pathResult?.path;
1609
- const registry = options?.extensionRegistry;
1610
- if (registry === void 0) {
1611
- return null;
1957
+ function provenanceForParsedTag(tag, sourceFile, file) {
1958
+ const { line, character } = sourceFile.getLineAndCharacterOfPosition(tag.tagNameSpan.start);
1959
+ return {
1960
+ surface: "tsdoc",
1961
+ file,
1962
+ line: line + 1,
1963
+ column: character,
1964
+ tagName: "@" + tag.normalizedTagName
1965
+ };
1966
+ }
1967
+ function provenanceForJSDocTag(tag, file) {
1968
+ const sourceFile = tag.getSourceFile();
1969
+ const { line, character } = sourceFile.getLineAndCharacterOfPosition(tag.getStart());
1970
+ return {
1971
+ surface: "tsdoc",
1972
+ file,
1973
+ line: line + 1,
1974
+ column: character,
1975
+ tagName: "@" + tag.tagName.text
1976
+ };
1977
+ }
1978
+ function getTagCommentText(tag) {
1979
+ if (tag.comment === void 0) {
1980
+ return void 0;
1612
1981
  }
1613
- const directTag = registry.findConstraintTag(tagName);
1614
- if (directTag !== void 0) {
1615
- return makeCustomConstraintNode(
1616
- directTag.extensionId,
1617
- directTag.registration.constraintName,
1618
- directTag.registration.parseValue(effectiveText),
1619
- provenance,
1620
- path3,
1621
- registry
1622
- );
1623
- }
1624
- if (!isBuiltinConstraintName(tagName)) {
1625
- return null;
1626
- }
1627
- const broadenedTypeId = getBroadenedCustomTypeId(options?.fieldType);
1628
- if (broadenedTypeId === void 0) {
1629
- return null;
1630
- }
1631
- const broadened = registry.findBuiltinConstraintBroadening(broadenedTypeId, tagName);
1632
- if (broadened === void 0) {
1633
- return null;
1634
- }
1635
- return makeCustomConstraintNode(
1636
- broadened.extensionId,
1637
- broadened.registration.constraintName,
1638
- broadened.registration.parseValue(effectiveText),
1639
- provenance,
1640
- path3,
1641
- registry
1642
- );
1643
- }
1644
- function getBroadenedCustomTypeId(fieldType) {
1645
- if (fieldType?.kind === "custom") {
1646
- return fieldType.typeId;
1647
- }
1648
- if (fieldType?.kind !== "union") {
1649
- return void 0;
1650
- }
1651
- const customMembers = fieldType.members.filter(
1652
- (member) => member.kind === "custom"
1653
- );
1654
- if (customMembers.length !== 1) {
1655
- return void 0;
1656
- }
1657
- const nonCustomMembers = fieldType.members.filter((member) => member.kind !== "custom");
1658
- const allOtherMembersAreNull = nonCustomMembers.every(
1659
- (member) => member.kind === "primitive" && member.primitiveKind === "null"
1660
- );
1661
- const customMember = customMembers[0];
1662
- return allOtherMembersAreNull && customMember !== void 0 ? customMember.typeId : void 0;
1663
- }
1664
- function makeCustomConstraintNode(extensionId, constraintName, payload, provenance, path3, registry) {
1665
- const constraintId = `${extensionId}/${constraintName}`;
1666
- const registration = registry.findConstraint(constraintId);
1667
- if (registration === void 0) {
1668
- throw new Error(
1669
- `Custom TSDoc tag resolved to unregistered constraint "${constraintId}". Register the constraint before using its tag.`
1670
- );
1671
- }
1672
- return {
1673
- kind: "constraint",
1674
- constraintKind: "custom",
1675
- constraintId,
1676
- payload,
1677
- compositionRule: registration.compositionRule,
1678
- ...path3 && { path: path3 },
1679
- provenance
1680
- };
1681
- }
1682
- function parseDefaultValueValue(text, provenance) {
1683
- const trimmed = text.trim();
1684
- let value;
1685
- if (trimmed === "null") {
1686
- value = null;
1687
- } else if (trimmed === "true") {
1688
- value = true;
1689
- } else if (trimmed === "false") {
1690
- value = false;
1691
- } else {
1692
- const parsed = tryParseJson(trimmed);
1693
- value = parsed !== null ? parsed : trimmed;
1694
- }
1695
- return {
1696
- kind: "annotation",
1697
- annotationKind: "defaultValue",
1698
- value,
1699
- provenance
1700
- };
1701
- }
1702
- function isMemberTargetDisplayName(text) {
1703
- return parseMemberTargetDisplayName(text) !== null;
1704
- }
1705
- function parseMemberTargetDisplayName(text) {
1706
- const match = /^:([^\s]+)\s+([\s\S]+)$/.exec(text);
1707
- if (!match?.[1] || !match[2]) return null;
1708
- return { target: match[1], label: match[2].trim() };
1709
- }
1710
- function provenanceForComment(range, sourceFile, file, tagName) {
1711
- const { line, character } = sourceFile.getLineAndCharacterOfPosition(range.pos);
1712
- return {
1713
- surface: "tsdoc",
1714
- file,
1715
- line: line + 1,
1716
- column: character,
1717
- tagName: "@" + tagName
1718
- };
1719
- }
1720
- function provenanceForJSDocTag(tag, file) {
1721
- const sourceFile = tag.getSourceFile();
1722
- const { line, character } = sourceFile.getLineAndCharacterOfPosition(tag.getStart());
1723
- return {
1724
- surface: "tsdoc",
1725
- file,
1726
- line: line + 1,
1727
- column: character,
1728
- tagName: "@" + tag.tagName.text
1729
- };
1730
- }
1731
- function getTagCommentText(tag) {
1732
- if (tag.comment === void 0) {
1733
- return void 0;
1734
- }
1735
- if (typeof tag.comment === "string") {
1736
- return tag.comment;
1982
+ if (typeof tag.comment === "string") {
1983
+ return tag.comment;
1737
1984
  }
1738
1985
  return ts.getTextOfJSDocComment(tag.comment);
1739
1986
  }
1740
1987
 
1741
1988
  // src/analyzer/jsdoc-constraints.ts
1989
+ function extractJSDocParseResult(node, file = "", options) {
1990
+ return parseTSDocTags(node, file, options);
1991
+ }
1742
1992
  function extractJSDocConstraintNodes(node, file = "", options) {
1743
- const result = parseTSDocTags(node, file, options);
1993
+ const result = extractJSDocParseResult(node, file, options);
1744
1994
  return [...result.constraints];
1745
1995
  }
1746
1996
  function extractJSDocAnnotationNodes(node, file = "", options) {
1747
- const result = parseTSDocTags(node, file, options);
1997
+ const result = extractJSDocParseResult(node, file, options);
1748
1998
  return [...result.annotations];
1749
1999
  }
1750
2000
  function extractDefaultValueAnnotation(initializer, file = "") {
@@ -1793,13 +2043,16 @@ var RESOLVING_TYPE_PLACEHOLDER = {
1793
2043
  properties: [],
1794
2044
  additionalProperties: true
1795
2045
  };
1796
- function makeParseOptions(extensionRegistry, fieldType) {
1797
- if (extensionRegistry === void 0 && fieldType === void 0) {
2046
+ function makeParseOptions(extensionRegistry, fieldType, checker, subjectType, hostType) {
2047
+ if (extensionRegistry === void 0 && fieldType === void 0 && checker === void 0 && subjectType === void 0 && hostType === void 0) {
1798
2048
  return void 0;
1799
2049
  }
1800
2050
  return {
1801
2051
  ...extensionRegistry !== void 0 && { extensionRegistry },
1802
- ...fieldType !== void 0 && { fieldType }
2052
+ ...fieldType !== void 0 && { fieldType },
2053
+ ...checker !== void 0 && { checker },
2054
+ ...subjectType !== void 0 && { subjectType },
2055
+ ...hostType !== void 0 && { hostType }
1803
2056
  };
1804
2057
  }
1805
2058
  function analyzeClassToIR(classDecl, checker, file = "", extensionRegistry) {
@@ -1807,11 +2060,15 @@ function analyzeClassToIR(classDecl, checker, file = "", extensionRegistry) {
1807
2060
  const fields = [];
1808
2061
  const fieldLayouts = [];
1809
2062
  const typeRegistry = {};
1810
- const annotations = extractJSDocAnnotationNodes(
2063
+ const diagnostics = [];
2064
+ const classType = checker.getTypeAtLocation(classDecl);
2065
+ const classDoc = extractJSDocParseResult(
1811
2066
  classDecl,
1812
2067
  file,
1813
- makeParseOptions(extensionRegistry)
2068
+ makeParseOptions(extensionRegistry, void 0, checker, classType, classType)
1814
2069
  );
2070
+ const annotations = [...classDoc.annotations];
2071
+ diagnostics.push(...classDoc.diagnostics);
1815
2072
  const visiting = /* @__PURE__ */ new Set();
1816
2073
  const instanceMethods = [];
1817
2074
  const staticMethods = [];
@@ -1823,6 +2080,8 @@ function analyzeClassToIR(classDecl, checker, file = "", extensionRegistry) {
1823
2080
  file,
1824
2081
  typeRegistry,
1825
2082
  visiting,
2083
+ diagnostics,
2084
+ classType,
1826
2085
  extensionRegistry
1827
2086
  );
1828
2087
  if (fieldNode) {
@@ -1847,6 +2106,7 @@ function analyzeClassToIR(classDecl, checker, file = "", extensionRegistry) {
1847
2106
  fieldLayouts,
1848
2107
  typeRegistry,
1849
2108
  ...annotations.length > 0 && { annotations },
2109
+ ...diagnostics.length > 0 && { diagnostics },
1850
2110
  instanceMethods,
1851
2111
  staticMethods
1852
2112
  };
@@ -1855,11 +2115,15 @@ function analyzeInterfaceToIR(interfaceDecl, checker, file = "", extensionRegist
1855
2115
  const name = interfaceDecl.name.text;
1856
2116
  const fields = [];
1857
2117
  const typeRegistry = {};
1858
- const annotations = extractJSDocAnnotationNodes(
2118
+ const diagnostics = [];
2119
+ const interfaceType = checker.getTypeAtLocation(interfaceDecl);
2120
+ const interfaceDoc = extractJSDocParseResult(
1859
2121
  interfaceDecl,
1860
2122
  file,
1861
- makeParseOptions(extensionRegistry)
2123
+ makeParseOptions(extensionRegistry, void 0, checker, interfaceType, interfaceType)
1862
2124
  );
2125
+ const annotations = [...interfaceDoc.annotations];
2126
+ diagnostics.push(...interfaceDoc.diagnostics);
1863
2127
  const visiting = /* @__PURE__ */ new Set();
1864
2128
  for (const member of interfaceDecl.members) {
1865
2129
  if (ts3.isPropertySignature(member)) {
@@ -1869,6 +2133,8 @@ function analyzeInterfaceToIR(interfaceDecl, checker, file = "", extensionRegist
1869
2133
  file,
1870
2134
  typeRegistry,
1871
2135
  visiting,
2136
+ diagnostics,
2137
+ interfaceType,
1872
2138
  extensionRegistry
1873
2139
  );
1874
2140
  if (fieldNode) {
@@ -1883,6 +2149,7 @@ function analyzeInterfaceToIR(interfaceDecl, checker, file = "", extensionRegist
1883
2149
  fieldLayouts,
1884
2150
  typeRegistry,
1885
2151
  ...annotations.length > 0 && { annotations },
2152
+ ...diagnostics.length > 0 && { diagnostics },
1886
2153
  instanceMethods: [],
1887
2154
  staticMethods: []
1888
2155
  };
@@ -1900,11 +2167,15 @@ function analyzeTypeAliasToIR(typeAlias, checker, file = "", extensionRegistry)
1900
2167
  const name = typeAlias.name.text;
1901
2168
  const fields = [];
1902
2169
  const typeRegistry = {};
1903
- const annotations = extractJSDocAnnotationNodes(
2170
+ const diagnostics = [];
2171
+ const aliasType = checker.getTypeAtLocation(typeAlias);
2172
+ const typeAliasDoc = extractJSDocParseResult(
1904
2173
  typeAlias,
1905
2174
  file,
1906
- makeParseOptions(extensionRegistry)
2175
+ makeParseOptions(extensionRegistry, void 0, checker, aliasType, aliasType)
1907
2176
  );
2177
+ const annotations = [...typeAliasDoc.annotations];
2178
+ diagnostics.push(...typeAliasDoc.diagnostics);
1908
2179
  const visiting = /* @__PURE__ */ new Set();
1909
2180
  for (const member of typeAlias.type.members) {
1910
2181
  if (ts3.isPropertySignature(member)) {
@@ -1914,6 +2185,8 @@ function analyzeTypeAliasToIR(typeAlias, checker, file = "", extensionRegistry)
1914
2185
  file,
1915
2186
  typeRegistry,
1916
2187
  visiting,
2188
+ diagnostics,
2189
+ aliasType,
1917
2190
  extensionRegistry
1918
2191
  );
1919
2192
  if (fieldNode) {
@@ -1929,12 +2202,13 @@ function analyzeTypeAliasToIR(typeAlias, checker, file = "", extensionRegistry)
1929
2202
  fieldLayouts: fields.map(() => ({})),
1930
2203
  typeRegistry,
1931
2204
  ...annotations.length > 0 && { annotations },
2205
+ ...diagnostics.length > 0 && { diagnostics },
1932
2206
  instanceMethods: [],
1933
2207
  staticMethods: []
1934
2208
  }
1935
2209
  };
1936
2210
  }
1937
- function analyzeFieldToIR(prop, checker, file, typeRegistry, visiting, extensionRegistry) {
2211
+ function analyzeFieldToIR(prop, checker, file, typeRegistry, visiting, diagnostics, hostType, extensionRegistry) {
1938
2212
  if (!ts3.isIdentifier(prop.name)) {
1939
2213
  return null;
1940
2214
  }
@@ -1949,7 +2223,8 @@ function analyzeFieldToIR(prop, checker, file, typeRegistry, visiting, extension
1949
2223
  typeRegistry,
1950
2224
  visiting,
1951
2225
  prop,
1952
- extensionRegistry
2226
+ extensionRegistry,
2227
+ diagnostics
1953
2228
  );
1954
2229
  const constraints = [];
1955
2230
  if (prop.type && !shouldEmitPrimitiveAliasDefinition(prop.type, checker)) {
@@ -1957,13 +2232,15 @@ function analyzeFieldToIR(prop, checker, file, typeRegistry, visiting, extension
1957
2232
  ...extractTypeAliasConstraintNodes(prop.type, checker, file, extensionRegistry)
1958
2233
  );
1959
2234
  }
1960
- constraints.push(
1961
- ...extractJSDocConstraintNodes(prop, file, makeParseOptions(extensionRegistry, type))
2235
+ const docResult = extractJSDocParseResult(
2236
+ prop,
2237
+ file,
2238
+ makeParseOptions(extensionRegistry, type, checker, tsType, hostType)
1962
2239
  );
2240
+ constraints.push(...docResult.constraints);
2241
+ diagnostics.push(...docResult.diagnostics);
1963
2242
  let annotations = [];
1964
- annotations.push(
1965
- ...extractJSDocAnnotationNodes(prop, file, makeParseOptions(extensionRegistry, type))
1966
- );
2243
+ annotations.push(...docResult.annotations);
1967
2244
  const defaultAnnotation = extractDefaultValueAnnotation(prop.initializer, file);
1968
2245
  if (defaultAnnotation && !annotations.some((a) => a.annotationKind === "defaultValue")) {
1969
2246
  annotations.push(defaultAnnotation);
@@ -1979,7 +2256,7 @@ function analyzeFieldToIR(prop, checker, file, typeRegistry, visiting, extension
1979
2256
  provenance
1980
2257
  };
1981
2258
  }
1982
- function analyzeInterfacePropertyToIR(prop, checker, file, typeRegistry, visiting, extensionRegistry) {
2259
+ function analyzeInterfacePropertyToIR(prop, checker, file, typeRegistry, visiting, diagnostics, hostType, extensionRegistry) {
1983
2260
  if (!ts3.isIdentifier(prop.name)) {
1984
2261
  return null;
1985
2262
  }
@@ -1994,7 +2271,8 @@ function analyzeInterfacePropertyToIR(prop, checker, file, typeRegistry, visitin
1994
2271
  typeRegistry,
1995
2272
  visiting,
1996
2273
  prop,
1997
- extensionRegistry
2274
+ extensionRegistry,
2275
+ diagnostics
1998
2276
  );
1999
2277
  const constraints = [];
2000
2278
  if (prop.type && !shouldEmitPrimitiveAliasDefinition(prop.type, checker)) {
@@ -2002,13 +2280,15 @@ function analyzeInterfacePropertyToIR(prop, checker, file, typeRegistry, visitin
2002
2280
  ...extractTypeAliasConstraintNodes(prop.type, checker, file, extensionRegistry)
2003
2281
  );
2004
2282
  }
2005
- constraints.push(
2006
- ...extractJSDocConstraintNodes(prop, file, makeParseOptions(extensionRegistry, type))
2283
+ const docResult = extractJSDocParseResult(
2284
+ prop,
2285
+ file,
2286
+ makeParseOptions(extensionRegistry, type, checker, tsType, hostType)
2007
2287
  );
2288
+ constraints.push(...docResult.constraints);
2289
+ diagnostics.push(...docResult.diagnostics);
2008
2290
  let annotations = [];
2009
- annotations.push(
2010
- ...extractJSDocAnnotationNodes(prop, file, makeParseOptions(extensionRegistry, type))
2011
- );
2291
+ annotations.push(...docResult.annotations);
2012
2292
  ({ type, annotations } = applyEnumMemberDisplayNames(type, annotations));
2013
2293
  return {
2014
2294
  kind: "field",
@@ -2137,7 +2417,7 @@ function getTypeNodeRegistrationName(typeNode) {
2137
2417
  }
2138
2418
  return null;
2139
2419
  }
2140
- function resolveTypeNode(type, checker, file, typeRegistry, visiting, sourceNode, extensionRegistry) {
2420
+ function resolveTypeNode(type, checker, file, typeRegistry, visiting, sourceNode, extensionRegistry, diagnostics) {
2141
2421
  const customType = resolveRegisteredCustomType(sourceNode, extensionRegistry, checker);
2142
2422
  if (customType) {
2143
2423
  return customType;
@@ -2149,7 +2429,8 @@ function resolveTypeNode(type, checker, file, typeRegistry, visiting, sourceNode
2149
2429
  typeRegistry,
2150
2430
  visiting,
2151
2431
  sourceNode,
2152
- extensionRegistry
2432
+ extensionRegistry,
2433
+ diagnostics
2153
2434
  );
2154
2435
  if (primitiveAlias) {
2155
2436
  return primitiveAlias;
@@ -2192,7 +2473,8 @@ function resolveTypeNode(type, checker, file, typeRegistry, visiting, sourceNode
2192
2473
  typeRegistry,
2193
2474
  visiting,
2194
2475
  sourceNode,
2195
- extensionRegistry
2476
+ extensionRegistry,
2477
+ diagnostics
2196
2478
  );
2197
2479
  }
2198
2480
  if (checker.isArrayType(type)) {
@@ -2203,15 +2485,24 @@ function resolveTypeNode(type, checker, file, typeRegistry, visiting, sourceNode
2203
2485
  typeRegistry,
2204
2486
  visiting,
2205
2487
  sourceNode,
2206
- extensionRegistry
2488
+ extensionRegistry,
2489
+ diagnostics
2207
2490
  );
2208
2491
  }
2209
2492
  if (isObjectType(type)) {
2210
- return resolveObjectType(type, checker, file, typeRegistry, visiting, extensionRegistry);
2493
+ return resolveObjectType(
2494
+ type,
2495
+ checker,
2496
+ file,
2497
+ typeRegistry,
2498
+ visiting,
2499
+ extensionRegistry,
2500
+ diagnostics
2501
+ );
2211
2502
  }
2212
2503
  return { kind: "primitive", primitiveKind: "string" };
2213
2504
  }
2214
- function tryResolveNamedPrimitiveAlias(type, checker, file, typeRegistry, visiting, sourceNode, extensionRegistry) {
2505
+ function tryResolveNamedPrimitiveAlias(type, checker, file, typeRegistry, visiting, sourceNode, extensionRegistry, diagnostics) {
2215
2506
  if (!(type.flags & (ts3.TypeFlags.String | ts3.TypeFlags.Number | ts3.TypeFlags.BigInt | ts3.TypeFlags.BigIntLiteral | ts3.TypeFlags.Boolean | ts3.TypeFlags.Null))) {
2216
2507
  return null;
2217
2508
  }
@@ -2239,7 +2530,8 @@ function tryResolveNamedPrimitiveAlias(type, checker, file, typeRegistry, visiti
2239
2530
  file,
2240
2531
  typeRegistry,
2241
2532
  visiting,
2242
- extensionRegistry
2533
+ extensionRegistry,
2534
+ diagnostics
2243
2535
  ),
2244
2536
  ...constraints.length > 0 && { constraints },
2245
2537
  ...annotations.length > 0 && { annotations },
@@ -2266,7 +2558,7 @@ function shouldEmitPrimitiveAliasDefinition(typeNode, checker) {
2266
2558
  const resolved = checker.getTypeFromTypeNode(aliasDecl.type);
2267
2559
  return !!(resolved.flags & (ts3.TypeFlags.String | ts3.TypeFlags.Number | ts3.TypeFlags.BigInt | ts3.TypeFlags.BigIntLiteral | ts3.TypeFlags.Boolean | ts3.TypeFlags.Null));
2268
2560
  }
2269
- function resolveAliasedPrimitiveTarget(type, checker, file, typeRegistry, visiting, extensionRegistry) {
2561
+ function resolveAliasedPrimitiveTarget(type, checker, file, typeRegistry, visiting, extensionRegistry, diagnostics) {
2270
2562
  const nestedAliasDecl = type.aliasSymbol?.declarations?.find(ts3.isTypeAliasDeclaration);
2271
2563
  if (nestedAliasDecl !== void 0) {
2272
2564
  return resolveAliasedPrimitiveTarget(
@@ -2275,12 +2567,22 @@ function resolveAliasedPrimitiveTarget(type, checker, file, typeRegistry, visiti
2275
2567
  file,
2276
2568
  typeRegistry,
2277
2569
  visiting,
2278
- extensionRegistry
2570
+ extensionRegistry,
2571
+ diagnostics
2279
2572
  );
2280
2573
  }
2281
- return resolveTypeNode(type, checker, file, typeRegistry, visiting, void 0, extensionRegistry);
2574
+ return resolveTypeNode(
2575
+ type,
2576
+ checker,
2577
+ file,
2578
+ typeRegistry,
2579
+ visiting,
2580
+ void 0,
2581
+ extensionRegistry,
2582
+ diagnostics
2583
+ );
2282
2584
  }
2283
- function resolveUnionType(type, checker, file, typeRegistry, visiting, sourceNode, extensionRegistry) {
2585
+ function resolveUnionType(type, checker, file, typeRegistry, visiting, sourceNode, extensionRegistry, diagnostics) {
2284
2586
  const typeName = getNamedTypeName(type);
2285
2587
  const namedDecl = getNamedTypeDeclaration(type);
2286
2588
  if (typeName && typeName in typeRegistry) {
@@ -2370,7 +2672,8 @@ function resolveUnionType(type, checker, file, typeRegistry, visiting, sourceNod
2370
2672
  typeRegistry,
2371
2673
  visiting,
2372
2674
  nonNullMembers[0].sourceNode ?? sourceNode,
2373
- extensionRegistry
2675
+ extensionRegistry,
2676
+ diagnostics
2374
2677
  );
2375
2678
  const result = hasNull ? {
2376
2679
  kind: "union",
@@ -2386,7 +2689,8 @@ function resolveUnionType(type, checker, file, typeRegistry, visiting, sourceNod
2386
2689
  typeRegistry,
2387
2690
  visiting,
2388
2691
  memberSourceNode ?? sourceNode,
2389
- extensionRegistry
2692
+ extensionRegistry,
2693
+ diagnostics
2390
2694
  )
2391
2695
  );
2392
2696
  if (hasNull) {
@@ -2394,7 +2698,7 @@ function resolveUnionType(type, checker, file, typeRegistry, visiting, sourceNod
2394
2698
  }
2395
2699
  return registerNamed({ kind: "union", members });
2396
2700
  }
2397
- function resolveArrayType(type, checker, file, typeRegistry, visiting, sourceNode, extensionRegistry) {
2701
+ function resolveArrayType(type, checker, file, typeRegistry, visiting, sourceNode, extensionRegistry, diagnostics) {
2398
2702
  const typeArgs = isTypeReference(type) ? type.typeArguments : void 0;
2399
2703
  const elementType = typeArgs?.[0];
2400
2704
  const elementSourceNode = extractArrayElementTypeNode(sourceNode, checker);
@@ -2405,11 +2709,12 @@ function resolveArrayType(type, checker, file, typeRegistry, visiting, sourceNod
2405
2709
  typeRegistry,
2406
2710
  visiting,
2407
2711
  elementSourceNode,
2408
- extensionRegistry
2712
+ extensionRegistry,
2713
+ diagnostics
2409
2714
  ) : { kind: "primitive", primitiveKind: "string" };
2410
2715
  return { kind: "array", items };
2411
2716
  }
2412
- function tryResolveRecordType(type, checker, file, typeRegistry, visiting, extensionRegistry) {
2717
+ function tryResolveRecordType(type, checker, file, typeRegistry, visiting, extensionRegistry, diagnostics) {
2413
2718
  if (type.getProperties().length > 0) {
2414
2719
  return null;
2415
2720
  }
@@ -2424,7 +2729,8 @@ function tryResolveRecordType(type, checker, file, typeRegistry, visiting, exten
2424
2729
  typeRegistry,
2425
2730
  visiting,
2426
2731
  void 0,
2427
- extensionRegistry
2732
+ extensionRegistry,
2733
+ diagnostics
2428
2734
  );
2429
2735
  return { kind: "record", valueType };
2430
2736
  }
@@ -2453,7 +2759,7 @@ function typeNodeContainsReference(type, targetName) {
2453
2759
  }
2454
2760
  }
2455
2761
  }
2456
- function resolveObjectType(type, checker, file, typeRegistry, visiting, extensionRegistry) {
2762
+ function resolveObjectType(type, checker, file, typeRegistry, visiting, extensionRegistry, diagnostics) {
2457
2763
  const typeName = getNamedTypeName(type);
2458
2764
  const namedTypeName = typeName ?? void 0;
2459
2765
  const namedDecl = getNamedTypeDeclaration(type);
@@ -2490,7 +2796,8 @@ function resolveObjectType(type, checker, file, typeRegistry, visiting, extensio
2490
2796
  file,
2491
2797
  typeRegistry,
2492
2798
  visiting,
2493
- extensionRegistry
2799
+ extensionRegistry,
2800
+ diagnostics
2494
2801
  );
2495
2802
  if (recordNode) {
2496
2803
  visiting.delete(type);
@@ -2518,6 +2825,7 @@ function resolveObjectType(type, checker, file, typeRegistry, visiting, extensio
2518
2825
  file,
2519
2826
  typeRegistry,
2520
2827
  visiting,
2828
+ diagnostics ?? [],
2521
2829
  extensionRegistry
2522
2830
  );
2523
2831
  for (const prop of type.getProperties()) {
@@ -2532,7 +2840,8 @@ function resolveObjectType(type, checker, file, typeRegistry, visiting, extensio
2532
2840
  typeRegistry,
2533
2841
  visiting,
2534
2842
  declaration,
2535
- extensionRegistry
2843
+ extensionRegistry,
2844
+ diagnostics
2536
2845
  );
2537
2846
  const fieldNodeInfo = fieldInfoMap?.get(prop.name);
2538
2847
  properties.push({
@@ -2562,7 +2871,7 @@ function resolveObjectType(type, checker, file, typeRegistry, visiting, extensio
2562
2871
  }
2563
2872
  return objectNode;
2564
2873
  }
2565
- function getNamedTypeFieldNodeInfoMap(type, checker, file, typeRegistry, visiting, extensionRegistry) {
2874
+ function getNamedTypeFieldNodeInfoMap(type, checker, file, typeRegistry, visiting, diagnostics, extensionRegistry) {
2566
2875
  const symbols = [type.getSymbol(), type.aliasSymbol].filter(
2567
2876
  (s) => s?.declarations != null && s.declarations.length > 0
2568
2877
  );
@@ -2572,6 +2881,7 @@ function getNamedTypeFieldNodeInfoMap(type, checker, file, typeRegistry, visitin
2572
2881
  const classDecl = declarations.find(ts3.isClassDeclaration);
2573
2882
  if (classDecl) {
2574
2883
  const map = /* @__PURE__ */ new Map();
2884
+ const hostType = checker.getTypeAtLocation(classDecl);
2575
2885
  for (const member of classDecl.members) {
2576
2886
  if (ts3.isPropertyDeclaration(member) && ts3.isIdentifier(member.name)) {
2577
2887
  const fieldNode = analyzeFieldToIR(
@@ -2580,6 +2890,8 @@ function getNamedTypeFieldNodeInfoMap(type, checker, file, typeRegistry, visitin
2580
2890
  file,
2581
2891
  typeRegistry,
2582
2892
  visiting,
2893
+ diagnostics,
2894
+ hostType,
2583
2895
  extensionRegistry
2584
2896
  );
2585
2897
  if (fieldNode) {
@@ -2601,6 +2913,8 @@ function getNamedTypeFieldNodeInfoMap(type, checker, file, typeRegistry, visitin
2601
2913
  file,
2602
2914
  typeRegistry,
2603
2915
  visiting,
2916
+ checker.getTypeAtLocation(interfaceDecl),
2917
+ diagnostics,
2604
2918
  extensionRegistry
2605
2919
  );
2606
2920
  }
@@ -2612,6 +2926,8 @@ function getNamedTypeFieldNodeInfoMap(type, checker, file, typeRegistry, visitin
2612
2926
  file,
2613
2927
  typeRegistry,
2614
2928
  visiting,
2929
+ checker.getTypeAtLocation(typeAliasDecl),
2930
+ diagnostics,
2615
2931
  extensionRegistry
2616
2932
  );
2617
2933
  }
@@ -2661,7 +2977,7 @@ function isNullishTypeNode(typeNode) {
2661
2977
  }
2662
2978
  return ts3.isLiteralTypeNode(typeNode) && (typeNode.literal.kind === ts3.SyntaxKind.NullKeyword || typeNode.literal.kind === ts3.SyntaxKind.UndefinedKeyword);
2663
2979
  }
2664
- function buildFieldNodeInfoMap(members, checker, file, typeRegistry, visiting, extensionRegistry) {
2980
+ function buildFieldNodeInfoMap(members, checker, file, typeRegistry, visiting, hostType, diagnostics, extensionRegistry) {
2665
2981
  const map = /* @__PURE__ */ new Map();
2666
2982
  for (const member of members) {
2667
2983
  if (ts3.isPropertySignature(member)) {
@@ -2671,6 +2987,8 @@ function buildFieldNodeInfoMap(members, checker, file, typeRegistry, visiting, e
2671
2987
  file,
2672
2988
  typeRegistry,
2673
2989
  visiting,
2990
+ diagnostics,
2991
+ hostType,
2674
2992
  extensionRegistry
2675
2993
  );
2676
2994
  if (fieldNode) {
@@ -2902,760 +3220,44 @@ function analyzeNamedTypeToIR(filePath, typeName, extensionRegistry) {
2902
3220
  }
2903
3221
 
2904
3222
  // src/validate/constraint-validator.ts
2905
- import { normalizeConstraintTagName as normalizeConstraintTagName2 } from "@formspec/core";
2906
- function addContradiction(ctx, message, primary, related) {
2907
- ctx.diagnostics.push({
2908
- code: "CONTRADICTING_CONSTRAINTS",
2909
- message,
2910
- severity: "error",
2911
- primaryLocation: primary,
2912
- relatedLocations: [related]
2913
- });
2914
- }
2915
- function addTypeMismatch(ctx, message, primary) {
2916
- ctx.diagnostics.push({
2917
- code: "TYPE_MISMATCH",
2918
- message,
2919
- severity: "error",
2920
- primaryLocation: primary,
2921
- relatedLocations: []
2922
- });
2923
- }
2924
- function addUnknownExtension(ctx, message, primary) {
2925
- ctx.diagnostics.push({
2926
- code: "UNKNOWN_EXTENSION",
2927
- message,
2928
- severity: "warning",
2929
- primaryLocation: primary,
2930
- relatedLocations: []
2931
- });
2932
- }
2933
- function addUnknownPathTarget(ctx, message, primary) {
2934
- ctx.diagnostics.push({
2935
- code: "UNKNOWN_PATH_TARGET",
2936
- message,
2937
- severity: "error",
2938
- primaryLocation: primary,
2939
- relatedLocations: []
2940
- });
2941
- }
2942
- function addConstraintBroadening(ctx, message, primary, related) {
2943
- ctx.diagnostics.push({
2944
- code: "CONSTRAINT_BROADENING",
2945
- message,
2946
- severity: "error",
2947
- primaryLocation: primary,
2948
- relatedLocations: [related]
2949
- });
2950
- }
2951
- function getExtensionIdFromConstraintId(constraintId) {
2952
- const separator = constraintId.lastIndexOf("/");
2953
- if (separator <= 0) {
2954
- return null;
2955
- }
2956
- return constraintId.slice(0, separator);
2957
- }
2958
- function findNumeric(constraints, constraintKind) {
2959
- return constraints.find((c) => c.constraintKind === constraintKind);
2960
- }
2961
- function findLength(constraints, constraintKind) {
2962
- return constraints.find((c) => c.constraintKind === constraintKind);
2963
- }
2964
- function findAllowedMembers(constraints) {
2965
- return constraints.filter(
2966
- (c) => c.constraintKind === "allowedMembers"
2967
- );
2968
- }
2969
- function findConstConstraints(constraints) {
2970
- return constraints.filter(
2971
- (c) => c.constraintKind === "const"
2972
- );
2973
- }
2974
- function jsonValueEquals(left, right) {
2975
- if (left === right) {
2976
- return true;
2977
- }
2978
- if (Array.isArray(left) || Array.isArray(right)) {
2979
- if (!Array.isArray(left) || !Array.isArray(right) || left.length !== right.length) {
2980
- return false;
2981
- }
2982
- return left.every((item, index) => jsonValueEquals(item, right[index]));
2983
- }
2984
- if (isJsonObject(left) || isJsonObject(right)) {
2985
- if (!isJsonObject(left) || !isJsonObject(right)) {
2986
- return false;
2987
- }
2988
- const leftKeys = Object.keys(left).sort();
2989
- const rightKeys = Object.keys(right).sort();
2990
- if (leftKeys.length !== rightKeys.length) {
2991
- return false;
2992
- }
2993
- return leftKeys.every((key, index) => {
2994
- const rightKey = rightKeys[index];
2995
- if (rightKey !== key) {
2996
- return false;
2997
- }
2998
- const leftValue = left[key];
2999
- const rightValue = right[rightKey];
3000
- return leftValue !== void 0 && rightValue !== void 0 && jsonValueEquals(leftValue, rightValue);
3001
- });
3002
- }
3003
- return false;
3004
- }
3005
- function isJsonObject(value) {
3006
- return typeof value === "object" && value !== null && !Array.isArray(value);
3007
- }
3008
- function isOrderedBoundConstraint(constraint) {
3009
- return constraint.constraintKind === "minimum" || constraint.constraintKind === "exclusiveMinimum" || constraint.constraintKind === "minLength" || constraint.constraintKind === "minItems" || constraint.constraintKind === "maximum" || constraint.constraintKind === "exclusiveMaximum" || constraint.constraintKind === "maxLength" || constraint.constraintKind === "maxItems";
3010
- }
3011
- function pathKey(constraint) {
3012
- return constraint.path?.segments.join(".") ?? "";
3013
- }
3014
- function orderedBoundFamily(kind) {
3015
- switch (kind) {
3016
- case "minimum":
3017
- case "exclusiveMinimum":
3018
- return "numeric-lower";
3019
- case "maximum":
3020
- case "exclusiveMaximum":
3021
- return "numeric-upper";
3022
- case "minLength":
3023
- return "minLength";
3024
- case "minItems":
3025
- return "minItems";
3026
- case "maxLength":
3027
- return "maxLength";
3028
- case "maxItems":
3029
- return "maxItems";
3030
- default: {
3031
- const _exhaustive = kind;
3032
- return _exhaustive;
3033
- }
3034
- }
3035
- }
3036
- function isNumericLowerKind(kind) {
3037
- return kind === "minimum" || kind === "exclusiveMinimum";
3038
- }
3039
- function isNumericUpperKind(kind) {
3040
- return kind === "maximum" || kind === "exclusiveMaximum";
3041
- }
3042
- function describeConstraintTag(constraint) {
3043
- return `@${constraint.constraintKind}`;
3044
- }
3045
- function compareConstraintStrength(current, previous) {
3046
- const family = orderedBoundFamily(current.constraintKind);
3047
- if (family === "numeric-lower") {
3048
- if (!isNumericLowerKind(current.constraintKind) || !isNumericLowerKind(previous.constraintKind)) {
3049
- throw new Error("numeric-lower family received non-numeric lower-bound constraint");
3050
- }
3051
- if (current.value !== previous.value) {
3052
- return current.value > previous.value ? 1 : -1;
3053
- }
3054
- if (current.constraintKind === "exclusiveMinimum" && previous.constraintKind === "minimum") {
3055
- return 1;
3056
- }
3057
- if (current.constraintKind === "minimum" && previous.constraintKind === "exclusiveMinimum") {
3058
- return -1;
3059
- }
3060
- return 0;
3061
- }
3062
- if (family === "numeric-upper") {
3063
- if (!isNumericUpperKind(current.constraintKind) || !isNumericUpperKind(previous.constraintKind)) {
3064
- throw new Error("numeric-upper family received non-numeric upper-bound constraint");
3065
- }
3066
- if (current.value !== previous.value) {
3067
- return current.value < previous.value ? 1 : -1;
3068
- }
3069
- if (current.constraintKind === "exclusiveMaximum" && previous.constraintKind === "maximum") {
3070
- return 1;
3071
- }
3072
- if (current.constraintKind === "maximum" && previous.constraintKind === "exclusiveMaximum") {
3073
- return -1;
3074
- }
3075
- return 0;
3076
- }
3077
- switch (family) {
3078
- case "minLength":
3079
- case "minItems":
3080
- if (current.value === previous.value) {
3081
- return 0;
3082
- }
3083
- return current.value > previous.value ? 1 : -1;
3084
- case "maxLength":
3085
- case "maxItems":
3086
- if (current.value === previous.value) {
3087
- return 0;
3088
- }
3089
- return current.value < previous.value ? 1 : -1;
3090
- default: {
3091
- const _exhaustive = family;
3092
- return _exhaustive;
3093
- }
3094
- }
3095
- }
3096
- function checkConstraintBroadening(ctx, fieldName, constraints) {
3097
- const strongestByKey = /* @__PURE__ */ new Map();
3098
- for (const constraint of constraints) {
3099
- if (!isOrderedBoundConstraint(constraint)) {
3100
- continue;
3101
- }
3102
- const key = `${orderedBoundFamily(constraint.constraintKind)}:${pathKey(constraint)}`;
3103
- const previous = strongestByKey.get(key);
3104
- if (previous === void 0) {
3105
- strongestByKey.set(key, constraint);
3106
- continue;
3107
- }
3108
- const strength = compareConstraintStrength(constraint, previous);
3109
- if (strength < 0) {
3110
- const displayFieldName = formatPathTargetFieldName(
3111
- fieldName,
3112
- constraint.path?.segments ?? []
3113
- );
3114
- addConstraintBroadening(
3115
- ctx,
3116
- `Field "${displayFieldName}": ${describeConstraintTag(constraint)} (${String(constraint.value)}) is broader than earlier ${describeConstraintTag(previous)} (${String(previous.value)}). Constraints can only narrow.`,
3117
- constraint.provenance,
3118
- previous.provenance
3119
- );
3120
- continue;
3121
- }
3122
- if (strength <= 0) {
3123
- continue;
3124
- }
3125
- strongestByKey.set(key, constraint);
3126
- }
3127
- }
3128
- function compareCustomConstraintStrength(current, previous) {
3129
- const order = current.comparePayloads(current.constraint.payload, previous.constraint.payload);
3130
- const equalPayloadTiebreaker = order === 0 ? compareSemanticInclusivity(current.role.inclusive, previous.role.inclusive) : order;
3131
- switch (current.role.bound) {
3132
- case "lower":
3133
- return equalPayloadTiebreaker;
3134
- case "upper":
3135
- return equalPayloadTiebreaker === 0 ? 0 : -equalPayloadTiebreaker;
3136
- case "exact":
3137
- return order === 0 ? 0 : Number.NaN;
3138
- default: {
3139
- const _exhaustive = current.role.bound;
3140
- return _exhaustive;
3141
- }
3142
- }
3143
- }
3144
- function compareSemanticInclusivity(currentInclusive, previousInclusive) {
3145
- if (currentInclusive === previousInclusive) {
3146
- return 0;
3147
- }
3148
- return currentInclusive ? -1 : 1;
3149
- }
3150
- function customConstraintsContradict(lower, upper) {
3151
- const order = lower.comparePayloads(lower.constraint.payload, upper.constraint.payload);
3152
- if (order > 0) {
3153
- return true;
3154
- }
3155
- if (order < 0) {
3156
- return false;
3157
- }
3158
- return !lower.role.inclusive || !upper.role.inclusive;
3159
- }
3160
- function describeCustomConstraintTag(constraint) {
3161
- return constraint.provenance.tagName ?? constraint.constraintId;
3162
- }
3163
- function checkCustomConstraintSemantics(ctx, fieldName, constraints) {
3164
- if (ctx.extensionRegistry === void 0) {
3165
- return;
3166
- }
3167
- const strongestByKey = /* @__PURE__ */ new Map();
3168
- const lowerByFamily = /* @__PURE__ */ new Map();
3169
- const upperByFamily = /* @__PURE__ */ new Map();
3170
- for (const constraint of constraints) {
3171
- if (constraint.constraintKind !== "custom") {
3172
- continue;
3173
- }
3174
- const registration = ctx.extensionRegistry.findConstraint(constraint.constraintId);
3175
- if (registration?.comparePayloads === void 0 || registration.semanticRole === void 0) {
3176
- continue;
3177
- }
3178
- const entry = {
3179
- constraint,
3180
- comparePayloads: registration.comparePayloads,
3181
- role: registration.semanticRole
3182
- };
3183
- const familyKey = `${registration.semanticRole.family}:${pathKey(constraint)}`;
3184
- const boundKey = `${familyKey}:${registration.semanticRole.bound}`;
3185
- const previous = strongestByKey.get(boundKey);
3186
- if (previous !== void 0) {
3187
- const strength = compareCustomConstraintStrength(entry, previous);
3188
- if (Number.isNaN(strength)) {
3189
- addContradiction(
3190
- ctx,
3191
- `Field "${formatPathTargetFieldName(fieldName, constraint.path?.segments ?? [])}": ${describeCustomConstraintTag(constraint)} conflicts with ${describeCustomConstraintTag(previous.constraint)}`,
3192
- constraint.provenance,
3193
- previous.constraint.provenance
3194
- );
3195
- continue;
3196
- }
3197
- if (strength < 0) {
3198
- addConstraintBroadening(
3199
- ctx,
3200
- `Field "${formatPathTargetFieldName(fieldName, constraint.path?.segments ?? [])}": ${describeCustomConstraintTag(constraint)} is broader than earlier ${describeCustomConstraintTag(previous.constraint)}. Constraints can only narrow.`,
3201
- constraint.provenance,
3202
- previous.constraint.provenance
3203
- );
3204
- continue;
3205
- }
3206
- if (strength > 0) {
3207
- strongestByKey.set(boundKey, entry);
3208
- }
3209
- } else {
3210
- strongestByKey.set(boundKey, entry);
3211
- }
3212
- if (registration.semanticRole.bound === "lower") {
3213
- lowerByFamily.set(familyKey, strongestByKey.get(boundKey) ?? entry);
3214
- } else if (registration.semanticRole.bound === "upper") {
3215
- upperByFamily.set(familyKey, strongestByKey.get(boundKey) ?? entry);
3216
- }
3217
- }
3218
- for (const [familyKey, lower] of lowerByFamily) {
3219
- const upper = upperByFamily.get(familyKey);
3220
- if (upper === void 0) {
3221
- continue;
3222
- }
3223
- if (!customConstraintsContradict(lower, upper)) {
3224
- continue;
3225
- }
3226
- addContradiction(
3227
- ctx,
3228
- `Field "${formatPathTargetFieldName(fieldName, lower.constraint.path?.segments ?? [])}": ${describeCustomConstraintTag(lower.constraint)} contradicts ${describeCustomConstraintTag(upper.constraint)}`,
3229
- lower.constraint.provenance,
3230
- upper.constraint.provenance
3231
- );
3232
- }
3233
- }
3234
- function checkNumericContradictions(ctx, fieldName, constraints) {
3235
- const min = findNumeric(constraints, "minimum");
3236
- const max = findNumeric(constraints, "maximum");
3237
- const exMin = findNumeric(constraints, "exclusiveMinimum");
3238
- const exMax = findNumeric(constraints, "exclusiveMaximum");
3239
- if (min !== void 0 && max !== void 0 && min.value > max.value) {
3240
- addContradiction(
3241
- ctx,
3242
- `Field "${fieldName}": minimum (${String(min.value)}) is greater than maximum (${String(max.value)})`,
3243
- min.provenance,
3244
- max.provenance
3245
- );
3246
- }
3247
- if (exMin !== void 0 && max !== void 0 && exMin.value >= max.value) {
3248
- addContradiction(
3249
- ctx,
3250
- `Field "${fieldName}": exclusiveMinimum (${String(exMin.value)}) is greater than or equal to maximum (${String(max.value)})`,
3251
- exMin.provenance,
3252
- max.provenance
3253
- );
3254
- }
3255
- if (min !== void 0 && exMax !== void 0 && min.value >= exMax.value) {
3256
- addContradiction(
3257
- ctx,
3258
- `Field "${fieldName}": minimum (${String(min.value)}) is greater than or equal to exclusiveMaximum (${String(exMax.value)})`,
3259
- min.provenance,
3260
- exMax.provenance
3261
- );
3262
- }
3263
- if (exMin !== void 0 && exMax !== void 0 && exMin.value >= exMax.value) {
3264
- addContradiction(
3265
- ctx,
3266
- `Field "${fieldName}": exclusiveMinimum (${String(exMin.value)}) is greater than or equal to exclusiveMaximum (${String(exMax.value)})`,
3267
- exMin.provenance,
3268
- exMax.provenance
3269
- );
3270
- }
3271
- }
3272
- function checkLengthContradictions(ctx, fieldName, constraints) {
3273
- const minLen = findLength(constraints, "minLength");
3274
- const maxLen = findLength(constraints, "maxLength");
3275
- if (minLen !== void 0 && maxLen !== void 0 && minLen.value > maxLen.value) {
3276
- addContradiction(
3277
- ctx,
3278
- `Field "${fieldName}": minLength (${String(minLen.value)}) is greater than maxLength (${String(maxLen.value)})`,
3279
- minLen.provenance,
3280
- maxLen.provenance
3281
- );
3282
- }
3283
- const minItems = findLength(constraints, "minItems");
3284
- const maxItems = findLength(constraints, "maxItems");
3285
- if (minItems !== void 0 && maxItems !== void 0 && minItems.value > maxItems.value) {
3286
- addContradiction(
3287
- ctx,
3288
- `Field "${fieldName}": minItems (${String(minItems.value)}) is greater than maxItems (${String(maxItems.value)})`,
3289
- minItems.provenance,
3290
- maxItems.provenance
3291
- );
3292
- }
3293
- }
3294
- function checkAllowedMembersContradiction(ctx, fieldName, constraints) {
3295
- const members = findAllowedMembers(constraints);
3296
- if (members.length < 2) return;
3297
- const firstSet = new Set(members[0]?.members ?? []);
3298
- for (let i = 1; i < members.length; i++) {
3299
- const current = members[i];
3300
- if (current === void 0) continue;
3301
- for (const m of firstSet) {
3302
- if (!current.members.includes(m)) {
3303
- firstSet.delete(m);
3304
- }
3305
- }
3306
- }
3307
- if (firstSet.size === 0) {
3308
- const first = members[0];
3309
- const second = members[1];
3310
- if (first !== void 0 && second !== void 0) {
3311
- addContradiction(
3312
- ctx,
3313
- `Field "${fieldName}": allowedMembers constraints have an empty intersection (no valid values remain)`,
3314
- first.provenance,
3315
- second.provenance
3316
- );
3317
- }
3318
- }
3319
- }
3320
- function checkConstContradictions(ctx, fieldName, constraints) {
3321
- const constConstraints = findConstConstraints(constraints);
3322
- if (constConstraints.length < 2) return;
3323
- const first = constConstraints[0];
3324
- if (first === void 0) return;
3325
- for (let i = 1; i < constConstraints.length; i++) {
3326
- const current = constConstraints[i];
3327
- if (current === void 0) continue;
3328
- if (jsonValueEquals(first.value, current.value)) {
3329
- continue;
3330
- }
3331
- addContradiction(
3332
- ctx,
3333
- `Field "${fieldName}": conflicting @const constraints require both ${JSON.stringify(first.value)} and ${JSON.stringify(current.value)}`,
3334
- first.provenance,
3335
- current.provenance
3336
- );
3337
- }
3338
- }
3339
- function typeLabel(type) {
3340
- switch (type.kind) {
3341
- case "primitive":
3342
- return type.primitiveKind;
3343
- case "enum":
3344
- return "enum";
3345
- case "array":
3346
- return "array";
3347
- case "object":
3348
- return "object";
3349
- case "record":
3350
- return "record";
3351
- case "union":
3352
- return "union";
3353
- case "reference":
3354
- return `reference(${type.name})`;
3355
- case "dynamic":
3356
- return `dynamic(${type.dynamicKind})`;
3357
- case "custom":
3358
- return `custom(${type.typeId})`;
3359
- default: {
3360
- const _exhaustive = type;
3361
- return String(_exhaustive);
3362
- }
3363
- }
3364
- }
3365
- function dereferenceType(ctx, type) {
3366
- let current = type;
3367
- const seen = /* @__PURE__ */ new Set();
3368
- while (current.kind === "reference") {
3369
- if (seen.has(current.name)) {
3370
- return current;
3371
- }
3372
- seen.add(current.name);
3373
- const definition = ctx.typeRegistry[current.name];
3374
- if (definition === void 0) {
3375
- return current;
3376
- }
3377
- current = definition.type;
3378
- }
3379
- return current;
3380
- }
3381
- function collectReferencedTypeConstraints(ctx, type) {
3382
- const collected = [];
3383
- let current = type;
3384
- const seen = /* @__PURE__ */ new Set();
3385
- while (current.kind === "reference") {
3386
- if (seen.has(current.name)) {
3387
- break;
3388
- }
3389
- seen.add(current.name);
3390
- const definition = ctx.typeRegistry[current.name];
3391
- if (definition === void 0) {
3392
- break;
3393
- }
3394
- if (definition.constraints !== void 0) {
3395
- collected.push(...definition.constraints);
3396
- }
3397
- current = definition.type;
3398
- }
3399
- return collected;
3400
- }
3401
- function resolvePathTargetType(ctx, type, segments) {
3402
- const effectiveType = dereferenceType(ctx, type);
3403
- if (segments.length === 0) {
3404
- return { kind: "resolved", type: effectiveType };
3405
- }
3406
- if (effectiveType.kind === "array") {
3407
- return resolvePathTargetType(ctx, effectiveType.items, segments);
3408
- }
3409
- if (effectiveType.kind === "object") {
3410
- const [segment, ...rest] = segments;
3411
- if (segment === void 0) {
3412
- throw new Error("Invariant violation: object path traversal requires a segment");
3413
- }
3414
- const property = effectiveType.properties.find((prop) => prop.name === segment);
3415
- if (property === void 0) {
3416
- return { kind: "missing-property", segment };
3417
- }
3418
- return resolvePathTargetType(ctx, property.type, rest);
3419
- }
3420
- return { kind: "unresolvable", type: effectiveType };
3421
- }
3422
- function isNullType(type) {
3423
- return type.kind === "primitive" && type.primitiveKind === "null";
3424
- }
3425
- function collectCustomConstraintCandidateTypes(ctx, type) {
3426
- const effectiveType = dereferenceType(ctx, type);
3427
- const candidates = [effectiveType];
3428
- if (effectiveType.kind === "array") {
3429
- candidates.push(...collectCustomConstraintCandidateTypes(ctx, effectiveType.items));
3430
- }
3431
- if (effectiveType.kind === "union") {
3432
- const memberTypes = effectiveType.members.map((member) => dereferenceType(ctx, member));
3433
- const nonNullMembers = memberTypes.filter((member) => !isNullType(member));
3434
- if (nonNullMembers.length === 1 && nonNullMembers.length < memberTypes.length) {
3435
- const [nullableMember] = nonNullMembers;
3436
- if (nullableMember !== void 0) {
3437
- candidates.push(...collectCustomConstraintCandidateTypes(ctx, nullableMember));
3438
- }
3439
- }
3440
- }
3441
- return candidates;
3442
- }
3443
- function formatPathTargetFieldName(fieldName, path3) {
3444
- return path3.length === 0 ? fieldName : `${fieldName}.${path3.join(".")}`;
3445
- }
3446
- function checkConstraintOnType(ctx, fieldName, type, constraint) {
3447
- const effectiveType = dereferenceType(ctx, type);
3448
- const isNumber = effectiveType.kind === "primitive" && ["number", "integer", "bigint"].includes(effectiveType.primitiveKind);
3449
- const isString = effectiveType.kind === "primitive" && effectiveType.primitiveKind === "string";
3450
- const isArray = effectiveType.kind === "array";
3451
- const isEnum = effectiveType.kind === "enum";
3452
- const arrayItemType = effectiveType.kind === "array" ? dereferenceType(ctx, effectiveType.items) : void 0;
3453
- const isStringArray = arrayItemType?.kind === "primitive" && arrayItemType.primitiveKind === "string";
3454
- const label = typeLabel(effectiveType);
3455
- const ck = constraint.constraintKind;
3456
- switch (ck) {
3457
- case "minimum":
3458
- case "maximum":
3459
- case "exclusiveMinimum":
3460
- case "exclusiveMaximum":
3461
- case "multipleOf": {
3462
- if (!isNumber) {
3463
- addTypeMismatch(
3464
- ctx,
3465
- `Field "${fieldName}": constraint "${ck}" is only valid on number fields, but field type is "${label}"`,
3466
- constraint.provenance
3467
- );
3468
- }
3469
- break;
3470
- }
3471
- case "minLength":
3472
- case "maxLength":
3473
- case "pattern": {
3474
- if (!isString && !isStringArray) {
3475
- addTypeMismatch(
3476
- ctx,
3477
- `Field "${fieldName}": constraint "${ck}" is only valid on string fields or string array items, but field type is "${label}"`,
3478
- constraint.provenance
3479
- );
3480
- }
3481
- break;
3482
- }
3483
- case "minItems":
3484
- case "maxItems":
3485
- case "uniqueItems": {
3486
- if (!isArray) {
3487
- addTypeMismatch(
3488
- ctx,
3489
- `Field "${fieldName}": constraint "${ck}" is only valid on array fields, but field type is "${label}"`,
3490
- constraint.provenance
3491
- );
3492
- }
3493
- break;
3494
- }
3495
- case "allowedMembers": {
3496
- if (!isEnum) {
3497
- addTypeMismatch(
3498
- ctx,
3499
- `Field "${fieldName}": constraint "allowedMembers" is only valid on enum fields, but field type is "${label}"`,
3500
- constraint.provenance
3501
- );
3502
- }
3503
- break;
3504
- }
3505
- case "const": {
3506
- const isPrimitiveConstType = effectiveType.kind === "primitive" && ["string", "number", "integer", "bigint", "boolean", "null"].includes(
3507
- effectiveType.primitiveKind
3508
- ) || effectiveType.kind === "enum";
3509
- if (!isPrimitiveConstType) {
3510
- addTypeMismatch(
3511
- ctx,
3512
- `Field "${fieldName}": constraint "const" is only valid on primitive or enum fields, but field type is "${label}"`,
3513
- constraint.provenance
3514
- );
3515
- break;
3516
- }
3517
- if (effectiveType.kind === "primitive") {
3518
- const valueType = constraint.value === null ? "null" : Array.isArray(constraint.value) ? "array" : typeof constraint.value;
3519
- const expectedValueType = effectiveType.primitiveKind === "integer" || effectiveType.primitiveKind === "bigint" ? "number" : effectiveType.primitiveKind;
3520
- if (valueType !== expectedValueType) {
3521
- addTypeMismatch(
3522
- ctx,
3523
- `Field "${fieldName}": @const value type "${valueType}" is incompatible with field type "${effectiveType.primitiveKind}"`,
3524
- constraint.provenance
3525
- );
3526
- }
3527
- break;
3528
- }
3529
- const memberValues = effectiveType.members.map((member) => member.value);
3530
- if (!memberValues.some((member) => jsonValueEquals(member, constraint.value))) {
3531
- addTypeMismatch(
3532
- ctx,
3533
- `Field "${fieldName}": @const value ${JSON.stringify(constraint.value)} is not one of the enum members`,
3534
- constraint.provenance
3535
- );
3536
- }
3537
- break;
3538
- }
3539
- case "custom": {
3540
- checkCustomConstraint(ctx, fieldName, effectiveType, constraint);
3541
- break;
3542
- }
3543
- default: {
3544
- const _exhaustive = constraint;
3545
- throw new Error(
3546
- `Unhandled constraint kind: ${_exhaustive.constraintKind}`
3547
- );
3548
- }
3549
- }
3550
- }
3551
- function checkTypeApplicability(ctx, fieldName, type, constraints) {
3552
- for (const constraint of constraints) {
3553
- if (constraint.path) {
3554
- const resolution = resolvePathTargetType(ctx, type, constraint.path.segments);
3555
- const targetFieldName = formatPathTargetFieldName(fieldName, constraint.path.segments);
3556
- if (resolution.kind === "missing-property") {
3557
- addUnknownPathTarget(
3558
- ctx,
3559
- `Field "${targetFieldName}": path-targeted constraint "${constraint.constraintKind}" references unknown path segment "${resolution.segment}"`,
3560
- constraint.provenance
3561
- );
3562
- continue;
3563
- }
3564
- if (resolution.kind === "unresolvable") {
3565
- addTypeMismatch(
3566
- ctx,
3567
- `Field "${targetFieldName}": path-targeted constraint "${constraint.constraintKind}" is invalid because type "${typeLabel(resolution.type)}" cannot be traversed`,
3568
- constraint.provenance
3569
- );
3570
- continue;
3571
- }
3572
- checkConstraintOnType(ctx, targetFieldName, resolution.type, constraint);
3573
- continue;
3574
- }
3575
- checkConstraintOnType(ctx, fieldName, type, constraint);
3576
- }
3577
- }
3578
- function checkCustomConstraint(ctx, fieldName, type, constraint) {
3579
- if (ctx.extensionRegistry === void 0) return;
3580
- const registration = ctx.extensionRegistry.findConstraint(constraint.constraintId);
3581
- if (registration === void 0) {
3582
- addUnknownExtension(
3583
- ctx,
3584
- `Field "${fieldName}": custom constraint "${constraint.constraintId}" is not registered in the extension registry`,
3585
- constraint.provenance
3586
- );
3587
- return;
3588
- }
3589
- const candidateTypes = collectCustomConstraintCandidateTypes(ctx, type);
3590
- const normalizedTagName = constraint.provenance.tagName === void 0 ? void 0 : normalizeConstraintTagName2(constraint.provenance.tagName.replace(/^@/, ""));
3591
- if (normalizedTagName !== void 0) {
3592
- const tagRegistration = ctx.extensionRegistry.findConstraintTag(normalizedTagName);
3593
- const extensionId = getExtensionIdFromConstraintId(constraint.constraintId);
3594
- if (extensionId !== null && tagRegistration?.extensionId === extensionId && tagRegistration.registration.constraintName === registration.constraintName && !candidateTypes.some(
3595
- (candidateType) => tagRegistration.registration.isApplicableToType?.(candidateType) !== false
3596
- )) {
3597
- addTypeMismatch(
3598
- ctx,
3599
- `Field "${fieldName}": custom constraint "${constraint.constraintId}" is not applicable to type "${typeLabel(type)}"`,
3600
- constraint.provenance
3601
- );
3602
- return;
3603
- }
3604
- }
3605
- if (registration.applicableTypes === null) {
3606
- if (!candidateTypes.some((candidateType) => registration.isApplicableToType?.(candidateType) !== false)) {
3607
- addTypeMismatch(
3608
- ctx,
3609
- `Field "${fieldName}": custom constraint "${constraint.constraintId}" is not applicable to type "${typeLabel(type)}"`,
3610
- constraint.provenance
3611
- );
3223
+ import {
3224
+ analyzeConstraintTargets
3225
+ } from "@formspec/analysis";
3226
+ function validateFieldNode(ctx, field) {
3227
+ const analysis = analyzeConstraintTargets(
3228
+ field.name,
3229
+ field.type,
3230
+ field.constraints,
3231
+ ctx.typeRegistry,
3232
+ ctx.extensionRegistry === void 0 ? void 0 : {
3233
+ extensionRegistry: ctx.extensionRegistry
3612
3234
  }
3613
- return;
3614
- }
3615
- const applicableTypes = registration.applicableTypes;
3616
- const matchesApplicableType = candidateTypes.some(
3617
- (candidateType) => applicableTypes.includes(candidateType.kind) && registration.isApplicableToType?.(candidateType) !== false
3618
3235
  );
3619
- if (!matchesApplicableType) {
3620
- addTypeMismatch(
3621
- ctx,
3622
- `Field "${fieldName}": custom constraint "${constraint.constraintId}" is not applicable to type "${typeLabel(type)}"`,
3623
- constraint.provenance
3624
- );
3625
- }
3626
- }
3627
- function validateFieldNode(ctx, field) {
3628
- validateConstraints(ctx, field.name, field.type, [
3629
- ...collectReferencedTypeConstraints(ctx, field.type),
3630
- ...field.constraints
3631
- ]);
3236
+ ctx.diagnostics.push(...analysis.diagnostics);
3632
3237
  if (field.type.kind === "object") {
3633
- for (const prop of field.type.properties) {
3634
- validateObjectProperty(ctx, field.name, prop);
3238
+ for (const property of field.type.properties) {
3239
+ validateObjectProperty(ctx, field.name, property);
3635
3240
  }
3636
3241
  }
3637
3242
  }
3638
- function validateObjectProperty(ctx, parentName, prop) {
3639
- const qualifiedName = `${parentName}.${prop.name}`;
3640
- validateConstraints(ctx, qualifiedName, prop.type, [
3641
- ...collectReferencedTypeConstraints(ctx, prop.type),
3642
- ...prop.constraints
3643
- ]);
3644
- if (prop.type.kind === "object") {
3645
- for (const nestedProp of prop.type.properties) {
3646
- validateObjectProperty(ctx, qualifiedName, nestedProp);
3243
+ function validateObjectProperty(ctx, parentName, property) {
3244
+ const qualifiedName = `${parentName}.${property.name}`;
3245
+ const analysis = analyzeConstraintTargets(
3246
+ qualifiedName,
3247
+ property.type,
3248
+ property.constraints,
3249
+ ctx.typeRegistry,
3250
+ ctx.extensionRegistry === void 0 ? void 0 : {
3251
+ extensionRegistry: ctx.extensionRegistry
3252
+ }
3253
+ );
3254
+ ctx.diagnostics.push(...analysis.diagnostics);
3255
+ if (property.type.kind === "object") {
3256
+ for (const nestedProperty of property.type.properties) {
3257
+ validateObjectProperty(ctx, qualifiedName, nestedProperty);
3647
3258
  }
3648
3259
  }
3649
3260
  }
3650
- function validateConstraints(ctx, name, type, constraints) {
3651
- checkNumericContradictions(ctx, name, constraints);
3652
- checkLengthContradictions(ctx, name, constraints);
3653
- checkAllowedMembersContradiction(ctx, name, constraints);
3654
- checkConstContradictions(ctx, name, constraints);
3655
- checkConstraintBroadening(ctx, name, constraints);
3656
- checkCustomConstraintSemantics(ctx, name, constraints);
3657
- checkTypeApplicability(ctx, name, type, constraints);
3658
- }
3659
3261
  function validateElement(ctx, element) {
3660
3262
  switch (element.kind) {
3661
3263
  case "field":
@@ -3672,8 +3274,8 @@ function validateElement(ctx, element) {
3672
3274
  }
3673
3275
  break;
3674
3276
  default: {
3675
- const _exhaustive = element;
3676
- throw new Error(`Unhandled element kind: ${_exhaustive.kind}`);
3277
+ const exhaustive = element;
3278
+ throw new Error(`Unhandled element kind: ${String(exhaustive)}`);
3677
3279
  }
3678
3280
  }
3679
3281
  }
@@ -3688,12 +3290,18 @@ function validateIR(ir, options) {
3688
3290
  }
3689
3291
  return {
3690
3292
  diagnostics: ctx.diagnostics,
3691
- valid: ctx.diagnostics.every((d) => d.severity !== "error")
3293
+ valid: ctx.diagnostics.every((diagnostic) => diagnostic.severity !== "error")
3692
3294
  };
3693
3295
  }
3694
3296
 
3695
3297
  // src/generators/class-schema.ts
3696
3298
  function generateClassSchemas(analysis, source, options) {
3299
+ const errorDiagnostics = analysis.diagnostics?.filter(
3300
+ (diagnostic) => diagnostic.severity === "error"
3301
+ );
3302
+ if (errorDiagnostics !== void 0 && errorDiagnostics.length > 0) {
3303
+ throw new Error(formatValidationError(errorDiagnostics));
3304
+ }
3697
3305
  const ir = canonicalizeTSDoc(analysis, source);
3698
3306
  const validationResult = validateIR(ir, {
3699
3307
  ...options?.extensionRegistry !== void 0 && {