+61
-15
packages/emitter/src/emitter.ts
+61
-15
packages/emitter/src/emitter.ts
···
369
369
return;
370
370
}
371
371
372
-
// Only string enums can be added as defs
372
+
// Only string enums (including token refs) can be added as defs
373
373
// Union refs (type: "union") must be inlined at usage sites
374
374
if (unionDef.type === "string" && (unionDef.knownValues || unionDef.enum)) {
375
375
const defName = name.charAt(0).toLowerCase() + name.slice(1);
···
380
380
code: "union-refs-not-allowed-as-def",
381
381
severity: "error",
382
382
message:
383
-
`Named unions of model references cannot be defined as standalone defs. ` +
384
-
`Use @inline to inline them at usage sites, or use string enums instead.`,
383
+
`Named unions of non-token model references cannot be defined as standalone defs. ` +
384
+
`Use @inline to inline them at usage sites, use @token models for known values, or use string literals.`,
385
385
target: union,
386
386
});
387
387
}
···
461
461
private unionToLexiconProperty(
462
462
unionType: Union,
463
463
prop?: ModelProperty,
464
+
isDefining?: boolean,
464
465
): LexObjectProperty | null {
465
466
const variants = this.parseUnionVariants(unionType);
466
467
···
503
504
(variants.stringLiterals.length > 0 &&
504
505
!variants.hasStringType &&
505
506
variants.unionRefs.length === 0 &&
507
+
variants.knownValueRefs.length === 0 &&
506
508
isClosed(this.program, unionType))
507
509
) {
508
510
const isClosedUnion = isClosed(this.program, unionType);
···
514
516
const minLength = getMinLength(this.program, unionType);
515
517
const maxGraphemes = getMaxGraphemes(this.program, unionType);
516
518
const minGraphemes = getMinGraphemes(this.program, unionType);
519
+
520
+
// Combine string literals and token refs for known values
521
+
const allKnownValues = [
522
+
...variants.stringLiterals,
523
+
...variants.knownValueRefs,
524
+
];
525
+
517
526
return {
518
527
type: "string",
519
-
[isClosedUnion ? "enum" : "knownValues"]: variants.stringLiterals,
528
+
[isClosedUnion ? "enum" : "knownValues"]: allKnownValues,
520
529
...(propDesc && { description: propDesc }),
521
530
...(defaultValue !== undefined &&
522
531
typeof defaultValue === "string" && { default: defaultValue }),
···
529
538
530
539
// Model reference union (including empty union with unknown)
531
540
if (variants.unionRefs.length > 0 || variants.hasUnknown) {
532
-
if (variants.stringLiterals.length > 0) {
541
+
if (variants.stringLiterals.length > 0 || variants.knownValueRefs.length > 0) {
533
542
this.program.reportDiagnostic({
534
543
code: "union-mixed-refs-literals",
535
544
severity: "error",
536
545
message:
537
-
`Union contains both model references and string literals. Lexicon unions must be either: ` +
538
-
`(1) model references only (type: "union"), ` +
539
-
`(2) string literals + string type (type: "string" with knownValues), or ` +
546
+
`Union contains both non-token model references and string literals/token refs. Lexicon unions must be either: ` +
547
+
`(1) non-token model references only (type: "union"), ` +
548
+
`(2) token refs + string literals + string type (type: "string" with knownValues), or ` +
540
549
`(3) integer literals + integer type (type: "integer" with knownValues). ` +
541
550
`Separate these into distinct fields or nested unions.`,
542
551
target: unionType,
···
609
618
const stringLiterals: string[] = [];
610
619
const numericLiterals: number[] = [];
611
620
const booleanLiterals: boolean[] = [];
621
+
const tokenModels: Model[] = [];
612
622
let hasStringType = false;
613
623
let hasUnknown = false;
614
624
615
625
for (const variant of unionType.variants.values()) {
616
626
switch (variant.type.kind) {
617
627
case "Model":
618
-
const ref = this.getModelReference(variant.type as Model);
619
-
if (ref) unionRefs.push(ref);
628
+
const model = variant.type as Model;
629
+
// Collect token models separately - they're treated differently based on hasStringType
630
+
if (isToken(this.program, model)) {
631
+
tokenModels.push(model);
632
+
} else {
633
+
const ref = this.getModelReference(model);
634
+
if (ref) unionRefs.push(ref);
635
+
}
620
636
break;
621
637
case "String":
622
638
stringLiterals.push((variant.type as StringLiteral).value);
···
641
657
}
642
658
}
643
659
660
+
// Validate: tokens must appear with | string
661
+
// Per Lexicon spec line 240: "unions can not reference token"
662
+
if (tokenModels.length > 0 && !hasStringType) {
663
+
this.program.reportDiagnostic({
664
+
code: "tokens-require-string",
665
+
severity: "error",
666
+
message:
667
+
"Tokens must be used with | string. Per Lexicon spec, tokens encode as string values and cannot appear in union refs.",
668
+
target: unionType,
669
+
});
670
+
}
671
+
672
+
// Token models become "known values" (always fully qualified refs)
673
+
const knownValueRefs = tokenModels
674
+
.map((m) => this.getModelReference(m, true))
675
+
.filter((ref): ref is string => ref !== null);
676
+
644
677
const isStringEnum =
645
-
stringLiterals.length > 0 && hasStringType && unionRefs.length === 0;
678
+
(stringLiterals.length > 0 || knownValueRefs.length > 0) &&
679
+
hasStringType &&
680
+
unionRefs.length === 0;
646
681
647
682
return {
648
683
unionRefs,
649
684
stringLiterals,
650
685
numericLiterals,
651
686
booleanLiterals,
687
+
knownValueRefs,
652
688
hasStringType,
653
689
hasUnknown,
654
690
isStringEnum,
···
1194
1230
}
1195
1231
}
1196
1232
1197
-
const unionDef = this.unionToLexiconProperty(unionType, prop);
1233
+
const unionDef = this.unionToLexiconProperty(unionType, prop, isDefining);
1198
1234
if (!unionDef) return null;
1199
1235
1200
1236
// Inherit description from union if no prop description and union is @inline
···
1395
1431
entity: Model | Union,
1396
1432
name: string | undefined,
1397
1433
namespace: Namespace | undefined,
1434
+
fullyQualified = false,
1398
1435
): string | null {
1399
1436
if (!name || !namespace || namespace.name === "TypeSpec") return null;
1400
1437
···
1414
1451
});
1415
1452
}
1416
1453
1417
-
// Local reference (same namespace)
1454
+
// For knownValues (fullyQualified=true), always use fully qualified refs
1455
+
if (fullyQualified) {
1456
+
return `${namespaceName}#${defName}`;
1457
+
}
1458
+
1459
+
// Local reference (same namespace) - use short ref
1418
1460
if (
1419
1461
this.currentLexiconId === namespaceName ||
1420
1462
this.currentLexiconId === `${namespaceName}.defs`
···
1427
1469
return namespaceName;
1428
1470
}
1429
1471
1472
+
// All other refs use fully qualified format
1430
1473
return `${namespaceName}#${defName}`;
1431
1474
}
1432
1475
1433
-
private getModelReference(model: Model): string | null {
1434
-
return this.getReference(model, model.name, model.namespace);
1476
+
private getModelReference(
1477
+
model: Model,
1478
+
fullyQualified = false,
1479
+
): string | null {
1480
+
return this.getReference(model, model.name, model.namespace, fullyQualified);
1435
1481
}
1436
1482
1437
1483
private getUnionReference(union: Union): string | null {
-22
packages/emitter/test/spec/basic/input/com/example/unionWithTokens.tsp
-22
packages/emitter/test/spec/basic/input/com/example/unionWithTokens.tsp
···
1
-
import "@typelex/emitter";
2
-
3
-
namespace com.example.unionWithTokens {
4
-
/** Tests union with token references */
5
-
model Main {
6
-
/** Reason can be a known token or unknown type */
7
-
@required
8
-
reason: (ReasonSpam | ReasonViolation | ReasonMisleading | unknown);
9
-
}
10
-
11
-
/** Spam: frequent unwanted promotion */
12
-
@token
13
-
model ReasonSpam {}
14
-
15
-
/** Direct violation of rules */
16
-
@token
17
-
model ReasonViolation {}
18
-
19
-
/** Misleading or deceptive content */
20
-
@token
21
-
model ReasonMisleading {}
22
-
}
-34
packages/emitter/test/spec/basic/output/com/example/unionWithTokens.json
-34
packages/emitter/test/spec/basic/output/com/example/unionWithTokens.json
···
1
-
{
2
-
"lexicon": 1,
3
-
"id": "com.example.unionWithTokens",
4
-
"defs": {
5
-
"main": {
6
-
"type": "object",
7
-
"description": "Tests union with token references",
8
-
"required": ["reason"],
9
-
"properties": {
10
-
"reason": {
11
-
"type": "union",
12
-
"description": "Reason can be a known token or unknown type",
13
-
"refs": [
14
-
"#reasonSpam",
15
-
"#reasonViolation",
16
-
"#reasonMisleading"
17
-
]
18
-
}
19
-
}
20
-
},
21
-
"reasonSpam": {
22
-
"type": "token",
23
-
"description": "Spam: frequent unwanted promotion"
24
-
},
25
-
"reasonViolation": {
26
-
"type": "token",
27
-
"description": "Direct violation of rules"
28
-
},
29
-
"reasonMisleading": {
30
-
"type": "token",
31
-
"description": "Misleading or deceptive content"
32
-
}
33
-
}
34
-
}