Skip to content
This repository was archived by the owner on Jun 30, 2026. It is now read-only.

Commit 44dbf42

Browse files
maorlegerCopilot
andcommitted
[typespec-ts] fix extensible enum model emission
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent b068e3f commit 44dbf42

4 files changed

Lines changed: 110 additions & 8 deletions

File tree

packages/typespec-ts/src/codegen/models.ts

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
11
import { Project, SourceFile } from "ts-morph";
2-
import type { TSCodeModel } from "../codemodel/index.js";
2+
import type { TSCodeModel, TSEnum } from "../codemodel/index.js";
33
import type { SdkContext } from "../utils/interfaces.js";
4+
import { addDeclaration } from "../framework/declaration.js";
5+
import { refkey } from "../framework/refkey.js";
46
import { buildHelperTypeLookup } from "../tcgcadapter/helperTypes.js";
57
import {
68
addSerializationFunctions,
9+
buildEnumTypes,
710
emitType,
811
getModelNamespaces,
912
getModelsPath
@@ -65,7 +68,7 @@ export function emitModelFiles(
6568
codeModel.settings.sourceRoot,
6669
enumType.namespace
6770
);
68-
emitType(sdkContext, rawEnum, sourceFile);
71+
emitEnumFromCodeModel(sdkContext, enumType, rawEnum as any, sourceFile);
6972
}
7073

7174
for (const unionType of codeModel.unions) {
@@ -133,6 +136,30 @@ function getTypeKey(name: string, namespace: string[]): string {
133136
return `${namespace.join("/")}:${name}`;
134137
}
135138

139+
function emitEnumFromCodeModel(
140+
sdkContext: SdkContext,
141+
enumType: TSEnum,
142+
rawEnum: any,
143+
sourceFile: SourceFile
144+
): void {
145+
const [enumDeclaration, knownValuesEnum] = buildEnumTypes(
146+
sdkContext,
147+
rawEnum,
148+
false,
149+
enumType.isExtensible
150+
);
151+
152+
if (enumDeclaration.name.startsWith("_")) {
153+
return;
154+
}
155+
156+
if (enumType.isExtensible) {
157+
addDeclaration(sourceFile, knownValuesEnum, refkey(rawEnum, "knownValues"));
158+
}
159+
160+
addDeclaration(sourceFile, enumDeclaration, rawEnum);
161+
}
162+
136163
function getOrCreateModelsFile(
137164
project: Project,
138165
sourceRoot: string,

packages/typespec-ts/src/modular/emitModels.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -601,7 +601,8 @@ function buildNullableType(context: SdkContext, type: SdkNullableType) {
601601
export function buildEnumTypes(
602602
context: SdkContext,
603603
type: SdkEnumType,
604-
reportMemberNameDiagnostic = false // if reportMemberNameDiagnostic is true, it will report diagnostic for enum member name
604+
reportMemberNameDiagnostic = false, // if reportMemberNameDiagnostic is true, it will report diagnostic for enum member name
605+
treatAsExtensible = isExtensibleEnum(context, type)
605606
): [TypeAliasDeclarationStructure, EnumDeclarationStructure] {
606607
const rawMembers = type.values.map((value) =>
607608
emitEnumMember(context, value, reportMemberNameDiagnostic)
@@ -617,14 +618,14 @@ export function buildEnumTypes(
617618
kind: StructureKind.TypeAlias,
618619
name: normalizeModelName(context, type),
619620
isExported: true,
620-
type: !isExtensibleEnum(context, type)
621+
type: !treatAsExtensible
621622
? type.values.map((v) => getTypeExpression(context, v)).join(" | ")
622623
: getTypeExpression(context, type.valueType)
623624
};
624625

625626
const docs = type.doc ? type.doc : "Type of " + enumAsUnion.name;
626627
enumAsUnion.docs =
627-
isExtensibleEnum(context, type) && type.doc
628+
treatAsExtensible && type.doc
628629
? [getExtensibleEnumDescription(context, type) ?? docs]
629630
: [docs];
630631
enumDeclaration.docs = type.doc

packages/typespec-ts/src/tcgcadapter/adapter.ts

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1203,9 +1203,6 @@ function hasModelUsage(usage: UsageFlags | undefined): boolean {
12031203
);
12041204
}
12051205

1206-
// TODO(strategy-b/U1): extensible enum filtering is a separate adaptEnums fix.
1207-
// Keep this migration scoped to helper-type registration so U1 can land in a
1208-
// focused follow-up.
12091206
function shouldAdaptEnum(
12101207
sdkContext: SdkContext,
12111208
enumType: SdkEnumType
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
import { describe, expect, it } from "vitest";
2+
import { Project } from "ts-morph";
3+
import { useContext } from "../../src/contextManager.js";
4+
import { useBinder } from "../../src/framework/hooks/binder.js";
5+
import { adaptToCodeModel } from "../../src/tcgcadapter/adapter.js";
6+
import { transformModularEmitterOptions } from "../../src/modular/buildModularOptions.js";
7+
import { emitModelFiles } from "../../src/codegen/models.js";
8+
import { renameClientName } from "../../src/index.js";
9+
import { createDpgContextTestHelper, rlcEmitterFor } from "../util/testUtil.js";
10+
11+
async function emitModels(body: string): Promise<string> {
12+
const typeSpec = `
13+
import "@typespec/http";
14+
import "@typespec/rest";
15+
import "@typespec/versioning";
16+
import "@azure-tools/typespec-client-generator-core";
17+
import "@azure-tools/typespec-azure-core";
18+
19+
using Http;
20+
using Rest;
21+
using Versioning;
22+
using Azure.ClientGenerator.Core;
23+
using Azure.Core;
24+
using Azure.Core.Traits;
25+
26+
@service(#{ title: "Azure TypeScript Testing" })
27+
namespace Azure.TypeScript.Testing {
28+
${body}
29+
}
30+
`;
31+
32+
const host = await rlcEmitterFor(typeSpec, { withRawContent: true });
33+
const sdkContext = await createDpgContextTestHelper(host.program, false, {
34+
isModularLibrary: true
35+
});
36+
sdkContext.rlcOptions!.isModularLibrary = true;
37+
38+
const emitterOptions = transformModularEmitterOptions(sdkContext, "", {
39+
casing: "camel"
40+
});
41+
for (const client of sdkContext.sdkPackage.clients) {
42+
await renameClientName(client, emitterOptions);
43+
}
44+
45+
const codeModel = adaptToCodeModel({ sdkContext, emitterOptions });
46+
const project = useContext("outputProject") as Project;
47+
emitModelFiles(project, codeModel, sdkContext);
48+
useBinder().resolveAllReferences(codeModel.settings.sourceRoot);
49+
50+
return project
51+
.getSourceFiles(`${codeModel.settings.sourceRoot}/models/**/*.ts`)
52+
.map((file) => file.getFullText())
53+
.join("\n");
54+
}
55+
56+
describe("models extensible enums", () => {
57+
it("emits KnownXxx declarations from enum IR", async () => {
58+
const modelsText = await emitModels(`
59+
union PetKind {
60+
dog: "dog",
61+
cat: "cat",
62+
string,
63+
}
64+
65+
model PetEnvelope {
66+
kind: PetKind;
67+
}
68+
69+
@route("/pets")
70+
@get
71+
op getPet(): PetEnvelope;
72+
`);
73+
74+
expect(modelsText).toContain("export enum KnownPetKind");
75+
expect(modelsText).toContain("export type PetKind = string;");
76+
});
77+
});

0 commit comments

Comments
 (0)