@@ -24,7 +24,6 @@ import {
2424 type ModelStatementNode ,
2525 type Node ,
2626 SyntaxKind ,
27- type UnionStatementNode ,
2827} from "@typespec/compiler/ast" ;
2928import { camelCase , constantCase , pascalCase , split , splitSeparateNumbers } from "change-case" ;
3029import { reportDiagnostic } from "../lib.js" ;
@@ -74,16 +73,18 @@ export function getTemplatedModelName(model: Model): string {
7473}
7574
7675function splitWithAcronyms (
77- split : ( name : string ) => string [ ] ,
76+ splitFn : ( name : string ) => string [ ] ,
7877 skipStart : boolean ,
7978 name : string ,
8079) : string [ ] {
81- const parts = split ( name ) ;
80+ const parts = splitFn ( name ) ;
8281
8382 if ( name === name . toUpperCase ( ) ) {
8483 return parts ;
8584 }
86- // Preserve strings of capital letters, e.g. "API" should be treated as three words ["A", "P", "I"] instead of one word
85+ // Split consecutive capital letters into individual characters for proper casing,
86+ // e.g. "API" becomes ["A", "P", "I"] so PascalCase produces "Api" → but we preserve
87+ // all-caps names at the toTypeName level, so this only affects mixed-case like "APIResponse".
8788 return parts . flatMap ( ( part , index ) => {
8889 if ( skipStart && index === 0 ) return part ;
8990 if ( part . match ( / ^ [ A - Z ] + $ / ) ) return part . split ( "" ) ;
@@ -140,45 +141,43 @@ function getNameWithoutNamespace(name: string): string {
140141
141142/** Generate a GraphQL type name for a union, including anonymous unions. */
142143export function getUnionName ( union : Union , program : Program ) : string {
143- // SyntaxKind.UnionExpression: Foo | Bar
144- // SyntaxKind.UnionStatement: union FooBarUnion { Foo, Bar }
145- // SyntaxKind.TypeReference: FooBarUnion
146-
147- const templateString = getTemplateString ( union ) ? "Of" + getTemplateString ( union ) : "" ;
148-
149- switch ( true ) {
150- case ! ! union . name :
151- // The union is not anonymous, use its name
152- return union . name ;
153-
154- case isReturnType ( union ) :
155- // The union is a return type, use the name of the operation
156- // e.g. op getBaz(): Foo | Bar => GetBazUnion
157- return `${ getUnionNameForOperation ( program , union ) } ${ templateString } Union` ;
158-
159- case isModelProperty ( union ) :
160- // The union is a model property, name it based on the model + property
161- // e.g. model Foo { bar: Bar | Baz } => FooBarUnion
162- const modelProperty = getModelProperty ( union ) ;
163- const propName = toTypeName ( getNameForNode ( modelProperty ! ) ) ;
164- const unionModel = union . node ?. parent ?. parent as ModelStatementNode ;
165- const modelName = unionModel ? getNameForNode ( unionModel ) : "" ;
166- return `${ modelName } ${ propName } ${ templateString } Union` ;
167-
168- case isAliased ( union ) :
169- // The union is an alias, name it based on the alias name
170- // e.g. alias Baz = Foo<string> | Bar => Baz
171- const alias = getAlias ( union ) ;
172- const aliasName = getNameForNode ( alias ! ) ;
173- return `${ aliasName } ${ templateString } ` ;
174-
175- default :
176- reportDiagnostic ( program , {
177- code : "unrecognized-union" ,
178- target : union ,
179- } ) ;
180- return "UnknownUnion" ;
144+ // Named union — use its name directly
145+ if ( union . name ) {
146+ return union . name ;
181147 }
148+
149+ const ts = getTemplateString ( union ) ;
150+ const templateString = ts ? "Of" + ts : "" ;
151+
152+ // Anonymous return type — name after the operation
153+ // e.g. op getBaz(): Foo | Bar => GetBazUnion
154+ if ( isReturnType ( union ) ) {
155+ return `${ getUnionNameForOperation ( program , union ) } ${ templateString } Union` ;
156+ }
157+
158+ // Anonymous model property — name after model + property
159+ // e.g. model Foo { bar: Bar | Baz } => FooBarUnion
160+ const modelProperty = getModelProperty ( union ) ;
161+ if ( modelProperty ) {
162+ const propName = toTypeName ( getNameForNode ( modelProperty ) ) ;
163+ const unionModel = union . node ?. parent ?. parent as ModelStatementNode ;
164+ const modelName = unionModel ? getNameForNode ( unionModel ) : "" ;
165+ return `${ modelName } ${ propName } ${ templateString } Union` ;
166+ }
167+
168+ // Alias — name after the alias
169+ // e.g. alias Baz = Foo<string> | Bar => Baz
170+ const alias = getAlias ( union ) ;
171+ if ( alias ) {
172+ const aliasName = getNameForNode ( alias ) ;
173+ return `${ aliasName } ${ templateString } ` ;
174+ }
175+
176+ reportDiagnostic ( program , {
177+ code : "unrecognized-union" ,
178+ target : union ,
179+ } ) ;
180+ return "UnknownUnion" ;
182181}
183182
184183function isNamedType ( type : Type | Value | IndeterminateEntity ) : type is { name : string } & Type {
@@ -216,8 +215,9 @@ function getNameForNode(node: NamedNode): string {
216215}
217216
218217function getUnionNameForOperation ( program : Program , union : Union ) : string {
219- const operationNode = ( union . node as UnionStatementNode ) . parent ?. parent ;
220- const operation = program . checker . getTypeForNode ( operationNode ! ) ;
218+ const operationNode = union . node ?. parent ?. parent ;
219+ if ( ! operationNode ) return "Unknown" ;
220+ const operation = program . checker . getTypeForNode ( operationNode ) ;
221221
222222 return toTypeName ( getTypeName ( operation ) ) ;
223223}
@@ -303,26 +303,19 @@ function getTemplateStringInternal(
303303 args : string [ ] ,
304304 options : { conjunction : string } = { conjunction : "And" } ,
305305) : string {
306+ // Apply toTypeName to convert raw compiler names (e.g., "string") to GraphQL PascalCase ("String")
306307 return args . length > 0 ? args . map ( toTypeName ) . join ( options . conjunction ) : "" ;
307308}
308309
309310/** Check if a model should be emitted as a GraphQL object type (not an array, record, or never). */
310311export function isTrueModel ( model : Model ) : boolean {
311- /* eslint-disable no-fallthrough */
312- switch ( true ) {
313- // A scalar array is represented as a model with an indexer
314- // and a scalar type. We don't want to emit this as a model.
315- case isScalarOrEnumArray ( model ) :
316- // A union array is represented as a model with an indexer
317- // and a union type. We don't want to emit this as a model.
318- case isUnionArray ( model ) :
319- case isNeverType ( model ) :
320- // If the model is purely a record, we don't want to emit it as a model.
321- // Instead, we will need to create a scalar
322- case isRecordType ( model ) && [ ...walkPropertiesInherited ( model ) ] . length === 0 :
323- return false ;
324- default :
325- return true ;
326- }
327- /* eslint-enable no-fallthrough */
312+ return ! (
313+ // Array of scalars/enums — represented as a list type, not an object type
314+ isScalarOrEnumArray ( model ) ||
315+ // Array of unions — represented as a list type, not an object type
316+ isUnionArray ( model ) ||
317+ isNeverType ( model ) ||
318+ // Pure record with no properties — emitted as a custom scalar, not an object type
319+ ( isRecordType ( model ) && [ ...walkPropertiesInherited ( model ) ] . length === 0 )
320+ ) ;
328321}
0 commit comments