@azure-tools/typespec-ts 0.56.1-dev.0 → 0.57.0-dev.10

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 (120) hide show
  1. package/README.md +6 -0
  2. package/dist/src/framework/hooks/sdk-types.d.ts.map +1 -1
  3. package/dist/src/framework/hooks/sdk-types.js +2 -0
  4. package/dist/src/framework/hooks/sdk-types.js.map +1 -1
  5. package/dist/src/interfaces.d.ts +4 -0
  6. package/dist/src/interfaces.d.ts.map +1 -1
  7. package/dist/src/interfaces.js.map +1 -1
  8. package/dist/src/lib.d.ts +11 -1
  9. package/dist/src/lib.d.ts.map +1 -1
  10. package/dist/src/lib.js +11 -0
  11. package/dist/src/lib.js.map +1 -1
  12. package/dist/src/metadata/build-package-file.d.ts +1 -0
  13. package/dist/src/metadata/build-package-file.d.ts.map +1 -1
  14. package/dist/src/metadata/build-package-file.js +3 -0
  15. package/dist/src/metadata/build-package-file.js.map +1 -1
  16. package/dist/src/metadata/package-json/build-azure-monorepo-package.d.ts +2 -0
  17. package/dist/src/metadata/package-json/build-azure-monorepo-package.d.ts.map +1 -1
  18. package/dist/src/metadata/package-json/build-azure-monorepo-package.js +1 -0
  19. package/dist/src/metadata/package-json/build-azure-monorepo-package.js.map +1 -1
  20. package/dist/src/modular/build-classical-client.d.ts.map +1 -1
  21. package/dist/src/modular/build-classical-client.js +19 -33
  22. package/dist/src/modular/build-classical-client.js.map +1 -1
  23. package/dist/src/modular/build-operations.d.ts.map +1 -1
  24. package/dist/src/modular/build-operations.js +4 -5
  25. package/dist/src/modular/build-operations.js.map +1 -1
  26. package/dist/src/modular/build-root-index.js +1 -1
  27. package/dist/src/modular/build-root-index.js.map +1 -1
  28. package/dist/src/modular/emit-models.d.ts.map +1 -1
  29. package/dist/src/modular/emit-models.js +46 -23
  30. package/dist/src/modular/emit-models.js.map +1 -1
  31. package/dist/src/modular/emit-samples.d.ts.map +1 -1
  32. package/dist/src/modular/emit-samples.js +29 -21
  33. package/dist/src/modular/emit-samples.js.map +1 -1
  34. package/dist/src/modular/emit-tests.js +2 -2
  35. package/dist/src/modular/emit-tests.js.map +1 -1
  36. package/dist/src/modular/helpers/classical-operation-helpers.d.ts.map +1 -1
  37. package/dist/src/modular/helpers/classical-operation-helpers.js +17 -15
  38. package/dist/src/modular/helpers/classical-operation-helpers.js.map +1 -1
  39. package/dist/src/modular/helpers/client-helpers.d.ts.map +1 -1
  40. package/dist/src/modular/helpers/client-helpers.js +7 -6
  41. package/dist/src/modular/helpers/client-helpers.js.map +1 -1
  42. package/dist/src/modular/helpers/example-value-helpers.d.ts +2 -1
  43. package/dist/src/modular/helpers/example-value-helpers.d.ts.map +1 -1
  44. package/dist/src/modular/helpers/example-value-helpers.js +34 -24
  45. package/dist/src/modular/helpers/example-value-helpers.js.map +1 -1
  46. package/dist/src/modular/helpers/naming-helpers.d.ts.map +1 -1
  47. package/dist/src/modular/helpers/naming-helpers.js +3 -3
  48. package/dist/src/modular/helpers/naming-helpers.js.map +1 -1
  49. package/dist/src/modular/helpers/operation-helpers.d.ts.map +1 -1
  50. package/dist/src/modular/helpers/operation-helpers.js +23 -18
  51. package/dist/src/modular/helpers/operation-helpers.js.map +1 -1
  52. package/dist/src/modular/helpers/type-helpers.d.ts.map +1 -1
  53. package/dist/src/modular/helpers/type-helpers.js +2 -2
  54. package/dist/src/modular/helpers/type-helpers.js.map +1 -1
  55. package/dist/src/modular/helpers/visibility-helpers.d.ts +7 -0
  56. package/dist/src/modular/helpers/visibility-helpers.d.ts.map +1 -0
  57. package/dist/src/modular/helpers/visibility-helpers.js +353 -0
  58. package/dist/src/modular/helpers/visibility-helpers.js.map +1 -0
  59. package/dist/src/modular/serialization/build-deserializer-function.js +6 -4
  60. package/dist/src/modular/serialization/build-deserializer-function.js.map +1 -1
  61. package/dist/src/modular/serialization/build-serializer-function.d.ts.map +1 -1
  62. package/dist/src/modular/serialization/build-serializer-function.js +9 -7
  63. package/dist/src/modular/serialization/build-serializer-function.js.map +1 -1
  64. package/dist/src/modular/serialization/build-xml-serializer-function.d.ts.map +1 -1
  65. package/dist/src/modular/serialization/build-xml-serializer-function.js +11 -13
  66. package/dist/src/modular/serialization/build-xml-serializer-function.js.map +1 -1
  67. package/dist/src/modular/type-expressions/get-type-expression.d.ts +1 -0
  68. package/dist/src/modular/type-expressions/get-type-expression.d.ts.map +1 -1
  69. package/dist/src/modular/type-expressions/get-type-expression.js +6 -4
  70. package/dist/src/modular/type-expressions/get-type-expression.js.map +1 -1
  71. package/dist/src/transform/transform-client-options.d.ts.map +1 -1
  72. package/dist/src/transform/transform-client-options.js +2 -0
  73. package/dist/src/transform/transform-client-options.js.map +1 -1
  74. package/dist/src/utils/client-utils.d.ts.map +1 -1
  75. package/dist/src/utils/client-utils.js +16 -5
  76. package/dist/src/utils/client-utils.js.map +1 -1
  77. package/dist/src/utils/cross-language-def.d.ts.map +1 -1
  78. package/dist/src/utils/cross-language-def.js +1 -2
  79. package/dist/src/utils/cross-language-def.js.map +1 -1
  80. package/dist/src/utils/interfaces.d.ts +1 -0
  81. package/dist/src/utils/interfaces.d.ts.map +1 -1
  82. package/dist/src/utils/name-utils.d.ts +15 -0
  83. package/dist/src/utils/name-utils.d.ts.map +1 -1
  84. package/dist/src/utils/name-utils.js +60 -0
  85. package/dist/src/utils/name-utils.js.map +1 -1
  86. package/dist/src/utils/operation-util.d.ts.map +1 -1
  87. package/dist/src/utils/operation-util.js +15 -5
  88. package/dist/src/utils/operation-util.js.map +1 -1
  89. package/dist/tsconfig.tsbuildinfo +1 -1
  90. package/package.json +31 -31
  91. package/src/framework/hooks/sdk-types.ts +2 -0
  92. package/src/interfaces.ts +4 -0
  93. package/src/lib.ts +13 -0
  94. package/src/metadata/build-package-file.ts +4 -0
  95. package/src/metadata/package-json/build-azure-monorepo-package.ts +1 -0
  96. package/src/modular/build-classical-client.ts +21 -32
  97. package/src/modular/build-operations.ts +4 -5
  98. package/src/modular/build-root-index.ts +1 -1
  99. package/src/modular/emit-models.ts +59 -24
  100. package/src/modular/emit-samples.ts +42 -34
  101. package/src/modular/emit-tests.ts +2 -2
  102. package/src/modular/helpers/classical-operation-helpers.ts +21 -20
  103. package/src/modular/helpers/client-helpers.ts +6 -5
  104. package/src/modular/helpers/example-value-helpers.ts +52 -41
  105. package/src/modular/helpers/naming-helpers.ts +8 -3
  106. package/src/modular/helpers/operation-helpers.ts +31 -18
  107. package/src/modular/helpers/type-helpers.ts +2 -5
  108. package/src/modular/helpers/visibility-helpers.ts +471 -0
  109. package/src/modular/serialization/build-deserializer-function.ts +6 -9
  110. package/src/modular/serialization/build-serializer-function.ts +14 -11
  111. package/src/modular/serialization/build-xml-serializer-function.ts +11 -17
  112. package/src/modular/type-expressions/get-type-expression.ts +10 -4
  113. package/src/transform/transform-client-options.ts +3 -0
  114. package/src/utils/client-utils.ts +28 -4
  115. package/src/utils/cross-language-def.ts +1 -2
  116. package/src/utils/interfaces.ts +1 -0
  117. package/src/utils/name-utils.ts +94 -0
  118. package/src/utils/operation-util.ts +27 -14
  119. package/static/static-helpers/storageCompatResponse.ts +3 -2
  120. package/static/static-helpers/urlTemplate.ts +4 -2
@@ -35,7 +35,13 @@ import { refkey } from "../../framework/refkey.js";
35
35
  import { reportDiagnostic } from "../../lib.js";
36
36
  import type { SdkContext } from "../../utils/interfaces.js";
37
37
  import { isAzureCoreErrorType } from "../../utils/model-utils.js";
38
- import { NameType, normalizeName } from "../../utils/name-utils.js";
38
+ import {
39
+ formatOptionalPropertyAccess,
40
+ formatPropertyAccess,
41
+ NameType,
42
+ normalizeSdkName,
43
+ normalizeSdkPropertyName,
44
+ } from "../../utils/name-utils.js";
39
45
  import {
40
46
  getCollectionFormatFromArrayEncoding,
41
47
  getCollectionFormatHelper,
@@ -1400,7 +1406,7 @@ export function getOperationOptionsName(
1400
1406
  includeGroupName && operation.name.indexOf("_") === -1
1401
1407
  ? getClassicalLayerPrefix(prefixes, NameType.Interface)
1402
1408
  : "";
1403
- const optionName = `${prefix}${normalizeName(operation.name, NameType.Interface)}OptionalParams`;
1409
+ const optionName = `${prefix}${normalizeSdkName(operation, NameType.Interface)}OptionalParams`;
1404
1410
  return optionName;
1405
1411
  }
1406
1412
 
@@ -1690,17 +1696,19 @@ function isContentType(param: SdkHttpParameter): boolean {
1690
1696
 
1691
1697
  function getContentTypeValue(param: SdkHttpParameter, optionalParamName: string = "options") {
1692
1698
  const defaultValue = param.clientDefaultValue;
1699
+ const optionAccessor = formatPropertyAccess(
1700
+ optionalParamName,
1701
+ normalizeSdkName(param, NameType.Parameter),
1702
+ );
1693
1703
  // allow customers to customize the content type if it's guessed by tcgc.
1694
1704
  if (isConstant(param.type)) {
1695
1705
  return `contentType: ${getConstantValue(param.type)}`;
1696
1706
  }
1697
1707
  if (defaultValue) {
1698
- return `contentType: ${optionalParamName}.${param.name} as any ?? "${defaultValue}"`;
1708
+ return `contentType: ${optionAccessor} as any ?? "${defaultValue}"`;
1699
1709
  } else {
1700
1710
  return `contentType: ${
1701
- !param.optional
1702
- ? normalizeName(param.name, NameType.Property)
1703
- : `${optionalParamName}.` + param.name + " as any"
1711
+ !param.optional ? normalizeSdkName(param, NameType.Property) : `${optionAccessor} as any`
1704
1712
  }`;
1705
1713
  }
1706
1714
  }
@@ -1919,10 +1927,11 @@ function getParamAccessor(param: SdkHttpParameter, optionalParamName: string = "
1919
1927
  if (param.onClient) {
1920
1928
  return `${clientPrefix}${getClientParameterName(param)}`;
1921
1929
  }
1930
+ const parameterName = normalizeSdkName(param, NameType.Parameter, { shouldGuard: true });
1922
1931
  if (getEffectiveOptional(param)) {
1923
- return `${optionalParamName}?.${param.name}`;
1932
+ return formatOptionalPropertyAccess(optionalParamName, parameterName);
1924
1933
  }
1925
- return param.name;
1934
+ return parameterName;
1926
1935
  }
1927
1936
 
1928
1937
  /**
@@ -1948,25 +1957,29 @@ function getMethodParamExpr(
1948
1957
  return undefined;
1949
1958
  }
1950
1959
 
1951
- const parts: string[] = [];
1960
+ let expression = "";
1952
1961
  for (let i = 0; i < path.length; i++) {
1953
1962
  const segment = path[i]!;
1954
1963
  if (i === 0) {
1955
1964
  // Normalize names for client-level segments to match the context interface property names
1956
1965
  const segmentName = segment.onClient
1957
1966
  ? getClientParameterName(segment as SdkMethodParameter)
1958
- : segment.name;
1967
+ : normalizeSdkName(segment, NameType.Parameter, { shouldGuard: true });
1959
1968
  if (segment.optional && !segment.onClient) {
1960
1969
  // If the first segment is optional and not on the client, we need to start with the optionalParamName
1961
- parts.push(`${optionalParamName}?.`);
1970
+ expression = formatOptionalPropertyAccess(optionalParamName, segmentName);
1971
+ } else {
1972
+ expression = segmentName;
1962
1973
  }
1963
- parts.push(segmentName);
1964
1974
  } else {
1965
1975
  const needsOptionalChain = path[i - 1]!.optional;
1966
- parts.push(`${needsOptionalChain ? "?." : "."}${segment.name}`);
1976
+ const segmentName = normalizeSdkPropertyName(segment);
1977
+ expression = needsOptionalChain
1978
+ ? formatOptionalPropertyAccess(expression, segmentName)
1979
+ : formatPropertyAccess(expression, segmentName);
1967
1980
  }
1968
1981
  }
1969
- return parts.join("");
1982
+ return expression;
1970
1983
  }
1971
1984
 
1972
1985
  function getPathParamExpr(
@@ -2692,14 +2705,14 @@ export function getPropertyFullName(
2692
2705
  ) {
2693
2706
  const normalizedPropertyName =
2694
2707
  propertyPath === ""
2695
- ? normalizeName(property.name, NameType.Parameter, true)
2696
- : normalizeModelPropertyName(context, property).replace(/^"/g, "").replace(/"$/g, "");
2708
+ ? normalizeSdkName(property, NameType.Parameter, { shouldGuard: true })
2709
+ : normalizeModelPropertyName(context, property);
2697
2710
 
2698
2711
  let fullName = normalizedPropertyName;
2699
2712
  if (propertyPath === "" && property.optional) {
2700
2713
  fullName = `options?.${normalizedPropertyName}`;
2701
2714
  } else if (propertyPath) {
2702
- fullName = `${propertyPath}["${normalizedPropertyName}"]`;
2715
+ fullName = `${propertyPath}[${normalizedPropertyName}]`;
2703
2716
  }
2704
2717
 
2705
2718
  return fullName;
@@ -2896,7 +2909,7 @@ export function getOperationResponseTypeName(method: [string[], ServiceOperation
2896
2909
  const prefix = !operation.name.includes("_")
2897
2910
  ? getClassicalLayerPrefix(prefixes, NameType.Interface)
2898
2911
  : "";
2899
- return `${prefix}${normalizeName(operation.name, NameType.Interface)}Response`;
2912
+ return `${prefix}${normalizeSdkName(operation, NameType.Interface)}Response`;
2900
2913
  }
2901
2914
 
2902
2915
  /**
@@ -7,7 +7,7 @@ import type {
7
7
  SdkType,
8
8
  } from "@azure-tools/typespec-client-generator-core";
9
9
  import type { SdkContext } from "../../utils/interfaces.js";
10
- import { NameType, normalizeName } from "../../utils/name-utils.js";
10
+ import { normalizeSdkPropertyName } from "../../utils/name-utils.js";
11
11
  import {
12
12
  getPropertyWithOverrides,
13
13
  type ModelOverrideOptions,
@@ -110,10 +110,7 @@ export function buildPropertyNameMapper(
110
110
  continue;
111
111
  }
112
112
  const prop = getPropertyWithOverrides(p, overrides);
113
- mapper.set(
114
- prop.serializationOptions.json?.name || prop.name,
115
- normalizeName(prop.name, NameType.Property),
116
- );
113
+ mapper.set(prop.serializationOptions.json?.name || prop.name, normalizeSdkPropertyName(prop));
117
114
  }
118
115
  return mapper;
119
116
  }
@@ -0,0 +1,471 @@
1
+ import type {
2
+ SdkHttpOperation,
3
+ SdkModelPropertyType,
4
+ SdkModelType,
5
+ SdkServiceMethod,
6
+ SdkType,
7
+ } from "@azure-tools/typespec-client-generator-core";
8
+ import { UsageFlags } from "@azure-tools/typespec-client-generator-core";
9
+ import {
10
+ Visibility,
11
+ getVisibilitySuffix,
12
+ isVisible,
13
+ resolveRequestVisibility,
14
+ } from "@typespec/http";
15
+
16
+ import { getAllOperationsFromClient } from "../../framework/hooks/sdk-types.js";
17
+ import type { SdkContext } from "../../utils/interfaces.js";
18
+ import { getDirectSubtypes } from "./type-helpers.js";
19
+
20
+ const responseUsageFlags =
21
+ UsageFlags.Output |
22
+ UsageFlags.Exception |
23
+ UsageFlags.LroInitial |
24
+ UsageFlags.LroPolling |
25
+ UsageFlags.LroFinalEnvelope;
26
+
27
+ interface SplitState {
28
+ context: SdkContext;
29
+ /**
30
+ * Every reachable `(model, visibility)` pair in the write-body graph, keyed by
31
+ * an emitter-local model identity plus visibility.
32
+ */
33
+ nodes: Map<string, ModelNode>;
34
+ /**
35
+ * The write clone materialized for each node that `needsClone`, keyed the same
36
+ * way as `nodes`.
37
+ */
38
+ cloneByKey: Map<string, SdkModelType>;
39
+ /** Stable emitter-local IDs preserving `SdkModelType` object identity. */
40
+ modelIds: WeakMap<SdkModelType, number>;
41
+ nextModelId: number;
42
+ }
43
+
44
+ /** A `(model, visibility)` vertex in the write-body reference graph. */
45
+ interface ModelNode {
46
+ model: SdkModelType;
47
+ visibility: Visibility;
48
+ /**
49
+ * Keys of referenced nodes. Discriminated hierarchy links are bidirectional
50
+ * so the whole hierarchy is projected together.
51
+ */
52
+ refKeys: Set<string>;
53
+ /** True if the model's own properties differ between `visibility` and Read. */
54
+ ownShapeDiffers: boolean;
55
+ /** True if this node can reach any shape-differing node. */
56
+ needsClone: boolean;
57
+ }
58
+
59
+ /**
60
+ * Projects request models whose write graph differs from their read graph.
61
+ * This must run before `visitPackageTypes` builds the emit queue.
62
+ */
63
+ export function applyVisibilityModelSplit(context: SdkContext): void {
64
+ if (!context.emitterOptions?.experimentalSplitModelsByVisibility) {
65
+ return;
66
+ }
67
+
68
+ const state = createSplitState(context);
69
+ const methods = context.sdkPackage.clients.flatMap((client) =>
70
+ getAllOperationsFromClient(client),
71
+ );
72
+
73
+ // Phase 1 — collect the reachable `(model, visibility)` reference graph.
74
+ for (const method of methods) {
75
+ collectMethodRoots(state, method);
76
+ }
77
+
78
+ // Phase 2 — mark models whose write graph differs from their read graph.
79
+ markNodesNeedingClones(state);
80
+
81
+ // Phase 3 — materialize projected models and wire their references.
82
+ buildClones(state);
83
+
84
+ // Phase 4 — project the original response models to the canonical Read view.
85
+ projectResponseModelsToRead(state);
86
+
87
+ // Phase 5 — repoint operation parameters and payloads to projected models.
88
+ for (const method of methods) {
89
+ repointMethodBody(state, method);
90
+ }
91
+
92
+ // Register projected models for the subsequent emit queue traversal.
93
+ for (const clone of state.cloneByKey.values()) {
94
+ context.sdkPackage.models.push(clone);
95
+ }
96
+ }
97
+
98
+ /** Creates the graph state using Read as the canonical model visibility. */
99
+ function createSplitState(context: SdkContext): SplitState {
100
+ return {
101
+ context,
102
+ nodes: new Map<string, ModelNode>(),
103
+ cloneByKey: new Map<string, SdkModelType>(),
104
+ modelIds: new WeakMap<SdkModelType, number>(),
105
+ nextModelId: 0,
106
+ };
107
+ }
108
+
109
+ /** Stable key identifying a `(model, visibility)` node (and its clone). */
110
+ function nodeKeyFor(state: SplitState, model: SdkModelType, visibility: Visibility): string {
111
+ let modelId = state.modelIds.get(model);
112
+ if (modelId === undefined) {
113
+ modelId = state.nextModelId++;
114
+ state.modelIds.set(model, modelId);
115
+ }
116
+ return `${modelId}|${visibility}`;
117
+ }
118
+
119
+ /**
120
+ * Collects the write-body roots of a single operation. The root of each
121
+ * `methodParameterSegments` entry is the client-method parameter.
122
+ */
123
+ function collectMethodRoots(state: SplitState, method: SdkServiceMethod<SdkHttpOperation>): void {
124
+ const operation = method.operation;
125
+ const bodyParam = operation?.bodyParam;
126
+ if (!bodyParam) {
127
+ return;
128
+ }
129
+
130
+ const visibility = resolveRequestVisibility(
131
+ state.context.program,
132
+ operation.__raw.operation,
133
+ operation.verb,
134
+ );
135
+
136
+ for (const segment of bodyParam.methodParameterSegments) {
137
+ const methodParam = segment[0];
138
+ if (!methodParam) {
139
+ continue;
140
+ }
141
+ collectReferencedModels(state, methodParam.type, visibility, new Set<string>());
142
+ }
143
+ }
144
+
145
+ /**
146
+ * Cycle-safe DFS that records a `(model, visibility)` node and its outgoing
147
+ * references. Nodes are registered before recursion so cyclic graphs terminate.
148
+ */
149
+ function collectNode(state: SplitState, model: SdkModelType, visibility: Visibility): string {
150
+ const key = nodeKeyFor(state, model, visibility);
151
+ if (state.nodes.has(key)) {
152
+ return key;
153
+ }
154
+
155
+ const node: ModelNode = {
156
+ model,
157
+ visibility,
158
+ refKeys: new Set<string>(),
159
+ ownShapeDiffers: false,
160
+ needsClone: false,
161
+ };
162
+ // Seed before recursing so cycles terminate at the already-seeded node.
163
+ state.nodes.set(key, node);
164
+
165
+ for (const property of model.properties) {
166
+ if (property.kind === "property" && property.__raw) {
167
+ const visibleInWrite = isVisible(state.context.program, property.__raw, visibility);
168
+ const visibleInRead = isVisible(state.context.program, property.__raw, Visibility.Read);
169
+ if (visibleInWrite !== visibleInRead) {
170
+ node.ownShapeDiffers = true;
171
+ }
172
+ if (!visibleInWrite) {
173
+ continue;
174
+ }
175
+ }
176
+
177
+ if (property.kind === "property") {
178
+ collectReferencedModels(state, property.type, visibility, node.refKeys);
179
+ }
180
+ }
181
+
182
+ if (model.additionalProperties) {
183
+ collectReferencedModels(state, model.additionalProperties, visibility, node.refKeys);
184
+ }
185
+
186
+ if (model.baseModel) {
187
+ node.refKeys.add(collectNode(state, model.baseModel, visibility));
188
+ }
189
+
190
+ // Link base and subtypes bidirectionally so the projected hierarchy stays together.
191
+ if (findDiscriminatedRoot(model)) {
192
+ for (const neighbor of discriminatedNeighbors(model)) {
193
+ const neighborKey = collectNode(state, neighbor, visibility);
194
+ node.refKeys.add(neighborKey);
195
+ state.nodes.get(neighborKey)!.refKeys.add(key);
196
+ }
197
+ }
198
+
199
+ return key;
200
+ }
201
+
202
+ /** Collects model references nested in supported container types. */
203
+ function collectReferencedModels(
204
+ state: SplitState,
205
+ type: SdkType,
206
+ visibility: Visibility,
207
+ refKeys: Set<string>,
208
+ ): void {
209
+ if (type.kind === "model") {
210
+ refKeys.add(collectNode(state, type, visibility));
211
+ return;
212
+ }
213
+ switch (type.kind) {
214
+ case "array":
215
+ case "dict":
216
+ collectReferencedModels(state, type.valueType, visibility, refKeys);
217
+ break;
218
+ case "nullable":
219
+ collectReferencedModels(state, type.type, visibility, refKeys);
220
+ break;
221
+ case "tuple":
222
+ for (const valueType of type.valueTypes) {
223
+ collectReferencedModels(state, valueType, visibility, refKeys);
224
+ }
225
+ break;
226
+ case "union":
227
+ for (const variantType of type.variantTypes) {
228
+ collectReferencedModels(state, variantType, visibility, refKeys);
229
+ }
230
+ break;
231
+ }
232
+ }
233
+
234
+ /** The base model and direct subtypes that share a node's discriminated tree. */
235
+ function discriminatedNeighbors(model: SdkModelType): SdkModelType[] {
236
+ const neighbors: SdkModelType[] = [];
237
+ if (model.baseModel) {
238
+ neighbors.push(model.baseModel);
239
+ }
240
+ neighbors.push(...getDirectSubtypes(model));
241
+ return neighbors;
242
+ }
243
+
244
+ /**
245
+ * Walks up the inheritance chain and returns the top-most ancestor that
246
+ * declares a discriminator.
247
+ */
248
+ function findDiscriminatedRoot(model: SdkModelType): SdkModelType | undefined {
249
+ let root: SdkModelType | undefined;
250
+ let current: SdkModelType | undefined = model;
251
+ while (current) {
252
+ if (current.discriminatedSubtypes) {
253
+ root = current;
254
+ }
255
+ current = current.baseModel;
256
+ }
257
+ return root;
258
+ }
259
+
260
+ /**
261
+ * Marks nodes whose write graph differs from the read graph by
262
+ * reverse-propagating from nodes whose own property membership differs.
263
+ */
264
+ function markNodesNeedingClones(state: SplitState): void {
265
+ // Reverse edges propagate a nested difference back to every parent.
266
+ const predecessors = new Map<string, string[]>();
267
+ for (const [key, node] of state.nodes) {
268
+ for (const referencedKey of node.refKeys) {
269
+ let referrers = predecessors.get(referencedKey);
270
+ if (!referrers) {
271
+ referrers = [];
272
+ predecessors.set(referencedKey, referrers);
273
+ }
274
+ referrers.push(key);
275
+ }
276
+ }
277
+
278
+ const queue: string[] = [];
279
+ for (const [key, node] of state.nodes) {
280
+ if (node.ownShapeDiffers) {
281
+ node.needsClone = true;
282
+ queue.push(key);
283
+ }
284
+ }
285
+ while (queue.length > 0) {
286
+ const key = queue.shift()!;
287
+ for (const predecessorKey of predecessors.get(key) ?? []) {
288
+ const predecessor = state.nodes.get(predecessorKey)!;
289
+ if (!predecessor.needsClone) {
290
+ predecessor.needsClone = true;
291
+ queue.push(predecessorKey);
292
+ }
293
+ }
294
+ }
295
+ }
296
+
297
+ /**
298
+ * Creates projected model shells, then wires properties and inheritance after
299
+ * every shell exists. This ordering lets forward, cyclic, and mutually recursive
300
+ * references resolve to projected models instead of the original read models.
301
+ */
302
+ function buildClones(state: SplitState): void {
303
+ // Pass 1: create every shell before wiring references.
304
+ for (const [key, node] of state.nodes) {
305
+ if (!node.needsClone) {
306
+ continue;
307
+ }
308
+ const clone: SdkModelType = {
309
+ ...node.model,
310
+ name: `${node.model.name}${getVisibilitySuffix(node.visibility, Visibility.Read)}`,
311
+ properties: [],
312
+ // Write projections are request-only: preserve serialization flags, force Input,
313
+ // and remove inherited response roles.
314
+ usage: (node.model.usage | UsageFlags.Input) & ~responseUsageFlags,
315
+ };
316
+ state.cloneByKey.set(key, clone);
317
+ }
318
+
319
+ // Pass 2: remove dropped properties and wire surviving references.
320
+ for (const [key, node] of state.nodes) {
321
+ const clone = state.cloneByKey.get(key);
322
+ if (!clone) {
323
+ continue;
324
+ }
325
+ const { model, visibility } = node;
326
+
327
+ const properties: SdkModelPropertyType[] = [];
328
+ for (const property of model.properties) {
329
+ if (
330
+ property.kind === "property" &&
331
+ property.__raw &&
332
+ !isVisible(state.context.program, property.__raw, visibility)
333
+ ) {
334
+ continue;
335
+ }
336
+ let wiredProperty = property;
337
+ if (property.kind === "property") {
338
+ const projectedType = projectTypeReferences(state, property.type, visibility);
339
+ if (projectedType !== property.type) {
340
+ wiredProperty = { ...property, type: projectedType };
341
+ }
342
+ }
343
+ properties.push(wiredProperty);
344
+ }
345
+ clone.properties = properties;
346
+
347
+ if (model.additionalProperties) {
348
+ clone.additionalProperties = projectTypeReferences(
349
+ state,
350
+ model.additionalProperties,
351
+ visibility,
352
+ );
353
+ }
354
+
355
+ if (model.baseModel) {
356
+ clone.baseModel =
357
+ state.cloneByKey.get(nodeKeyFor(state, model.baseModel, visibility)) ?? model.baseModel;
358
+ }
359
+
360
+ if (findDiscriminatedRoot(model)) {
361
+ if (model.discriminatedSubtypes) {
362
+ clone.discriminatedSubtypes = Object.fromEntries(
363
+ Object.entries(model.discriminatedSubtypes).map(([discriminatorValue, subtype]) => [
364
+ discriminatorValue,
365
+ state.cloneByKey.get(nodeKeyFor(state, subtype, visibility)) ?? subtype,
366
+ ]),
367
+ );
368
+ }
369
+ }
370
+ }
371
+ }
372
+
373
+ /** Projects response-used models to the canonical Read visibility. */
374
+ function projectResponseModelsToRead(state: SplitState): void {
375
+ for (const model of state.context.sdkPackage.models) {
376
+ if ((model.usage & responseUsageFlags) === 0) {
377
+ continue;
378
+ }
379
+ model.properties = model.properties.filter(
380
+ (property) =>
381
+ property.kind !== "property" ||
382
+ !property.__raw ||
383
+ isVisible(state.context.program, property.__raw, Visibility.Read),
384
+ );
385
+ }
386
+ }
387
+
388
+ /** Replaces model references nested in supported container types. */
389
+ function projectTypeReferences(state: SplitState, type: SdkType, visibility: Visibility): SdkType {
390
+ if (type.kind === "model") {
391
+ return state.cloneByKey.get(nodeKeyFor(state, type, visibility)) ?? type;
392
+ }
393
+ switch (type.kind) {
394
+ case "array":
395
+ case "dict": {
396
+ const projectedValueType = projectTypeReferences(state, type.valueType, visibility);
397
+ return projectedValueType === type.valueType
398
+ ? type
399
+ : { ...type, valueType: projectedValueType };
400
+ }
401
+ case "nullable": {
402
+ const projectedType = projectTypeReferences(state, type.type, visibility);
403
+ return projectedType === type.type ? type : { ...type, type: projectedType };
404
+ }
405
+ case "tuple": {
406
+ const projectedValueTypes = type.valueTypes.map((valueType) =>
407
+ projectTypeReferences(state, valueType, visibility),
408
+ );
409
+ return projectedValueTypes.every((valueType, index) => valueType === type.valueTypes[index])
410
+ ? type
411
+ : { ...type, valueTypes: projectedValueTypes };
412
+ }
413
+ case "union": {
414
+ const projectedVariantTypes = type.variantTypes.map((variantType) =>
415
+ projectTypeReferences(state, variantType, visibility),
416
+ );
417
+ return projectedVariantTypes.every(
418
+ (variantType, index) => variantType === type.variantTypes[index],
419
+ )
420
+ ? type
421
+ : { ...type, variantTypes: projectedVariantTypes };
422
+ }
423
+ }
424
+ return type;
425
+ }
426
+
427
+ /**
428
+ * Repoints client-method parameters and their HTTP payload representation to
429
+ * projected models.
430
+ */
431
+ function repointMethodBody(state: SplitState, method: SdkServiceMethod<SdkHttpOperation>): void {
432
+ const operation = method.operation;
433
+ const bodyParam = operation?.bodyParam;
434
+ if (!bodyParam) {
435
+ return;
436
+ }
437
+
438
+ const visibility = resolveRequestVisibility(
439
+ state.context.program,
440
+ operation.__raw.operation,
441
+ operation.verb,
442
+ );
443
+
444
+ for (const segment of bodyParam.methodParameterSegments) {
445
+ const methodParam = segment[0];
446
+ if (!methodParam) {
447
+ continue;
448
+ }
449
+
450
+ const originalType = methodParam.type;
451
+ const projectedType = projectTypeReferences(state, originalType, visibility);
452
+ if (projectedType === originalType) {
453
+ // The write view is identical to the read model (collapse).
454
+ continue;
455
+ }
456
+
457
+ methodParam.type = projectedType;
458
+
459
+ if (originalType === bodyParam.type) {
460
+ // A non-spread body uses the method parameter model as the payload model.
461
+ bodyParam.type = projectedType;
462
+ } else if (bodyParam.type.kind === "model") {
463
+ // A spread body uses a synthesized wrapper serialized property by property.
464
+ for (const bodyProperty of bodyParam.type.properties) {
465
+ if (bodyProperty.type === originalType) {
466
+ bodyProperty.type = projectedType;
467
+ }
468
+ }
469
+ }
470
+ }
471
+ }
@@ -184,7 +184,6 @@ function buildPolymorphicDeserializer(
184
184
  ) {
185
185
  return;
186
186
  }
187
- const union = subType?.discriminatedSubtypes ? "_Union" : "";
188
187
  if (!subType || !subType?.name) {
189
188
  reportDiagnostic(context.program, {
190
189
  code: "anonymous-type-deserialization",
@@ -192,14 +191,12 @@ function buildPolymorphicDeserializer(
192
191
  });
193
192
  return; // Skip this subtype
194
193
  }
195
-
196
- const rawSubTypeName = `${subType.name}${union}`;
197
- const subTypeName = `${normalizeName(rawSubTypeName, NameType.Interface, true)}`;
198
- const subtypeDeserializerName = normalizeName(
199
- `${subTypeName}Deserializer`,
200
- NameType.Operation,
201
- true,
202
- );
194
+ const union = subType.discriminatedSubtypes ? "Union" : "";
195
+ const subTypeName = normalizeModelName(context, subType, NameType.Interface, !union);
196
+ const subtypeDeserializerName = buildModelDeserializer(context, subType, {
197
+ nameOnly: true,
198
+ skipDiscriminatedUnionSuffix: false,
199
+ }) as string;
203
200
 
204
201
  cases.push(`
205
202
  case "${discriminatedValue}":