Skip to content

[tcgc] convert to same type for array or dictionary of same type #2626

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
May 7, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .chronus/changes/tcgc-cache_array_map-2025-4-6-18-37-49.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
changeKind: fix
packages:
- "@azure-tools/typespec-client-generator-core"
---

Convert to same type for array or dictionary of same type.
3 changes: 3 additions & 0 deletions packages/typespec-client-generator-core/src/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,9 @@ import { defaultDecoratorsAllowList } from "./configs.js";
import { handleClientExamples } from "./example.js";
import {
getKnownScalars,
SdkArrayType,
SdkContext,
SdkDictionaryType,
SdkEnumType,
SdkHttpOperation,
SdkModelPropertyType,
Expand Down Expand Up @@ -56,6 +58,7 @@ export function createTCGCContext(program: Program, emitterName?: string): TCGCC
Type,
SdkModelType | SdkEnumType | SdkUnionType | SdkNullableType
>(),
__arrayDictionaryCache: new Map<Type, SdkDictionaryType | SdkArrayType>(),
__modelPropertyCache: new Map<ModelProperty, SdkModelPropertyType>(),
__generatedNames: new Map<Union | Model | TspLiteralType, string>(),
__httpOperationCache: new Map<Operation, HttpOperation>(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ export interface TCGCContext {
flattenUnionAsEnum?: boolean;

__referencedTypeCache: Map<Type, SdkModelType | SdkEnumType | SdkUnionType | SdkNullableType>;
__arrayDictionaryCache: Map<Type, SdkDictionaryType | SdkArrayType>;
__modelPropertyCache: Map<ModelProperty, SdkModelPropertyType>;
__generatedNames: Map<Type, string>;
__httpOperationCache: Map<Operation, HttpOperation>;
Expand Down
54 changes: 30 additions & 24 deletions packages/typespec-client-generator-core/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -437,32 +437,38 @@ export function getSdkArrayOrDictWithDiagnostics(
// if model with both indexer and properties or name should be a model with additional properties
if (type.indexer !== undefined && type.properties.size === 0) {
if (!isNeverType(type.indexer.key)) {
const valueType = diagnostics.pipe(
getClientTypeWithDiagnostics(context, type.indexer.value!, operation),
);
const name = type.indexer.key.name;
if (name === "string" && type.name === "Record") {
// model MyModel is Record<> {} should be model with additional properties
if (type.sourceModel?.kind === "Model" && type.sourceModel?.name === "Record") {
return diagnostics.wrap(undefined);
let sdkType = context.__arrayDictionaryCache.get(type);
if (!sdkType) {
const valueType = diagnostics.pipe(
getClientTypeWithDiagnostics(context, type.indexer.value!, operation),
);
const name = type.indexer.key.name;
if (name === "string" && type.name === "Record") {
// model MyModel is Record<> {} should be model with additional properties
if (type.sourceModel?.kind === "Model" && type.sourceModel?.name === "Record") {
return diagnostics.wrap(undefined);
} else {
// other cases are dict
sdkType = {
...diagnostics.pipe(getSdkTypeBaseHelper(context, type, "dict")),
keyType: diagnostics.pipe(
getClientTypeWithDiagnostics(context, type.indexer.key, operation),
),
valueType: valueType,
};
}
} else if (name === "integer") {
// only array's index key name is integer
sdkType = {
...diagnostics.pipe(getSdkTypeBaseHelper(context, type, "array")),
name: getLibraryName(context, type),
valueType: valueType,
crossLanguageDefinitionId: getCrossLanguageDefinitionId(context, type, operation),
};
}
// other cases are dict
return diagnostics.wrap({
...diagnostics.pipe(getSdkTypeBaseHelper(context, type, "dict")),
keyType: diagnostics.pipe(
getClientTypeWithDiagnostics(context, type.indexer.key, operation),
),
valueType: valueType,
});
} else if (name === "integer") {
// only array's index key name is integer
return diagnostics.wrap({
...diagnostics.pipe(getSdkTypeBaseHelper(context, type, "array")),
name: getLibraryName(context, type),
valueType: valueType,
crossLanguageDefinitionId: getCrossLanguageDefinitionId(context, type, operation),
});
context.__arrayDictionaryCache.set(type, sdkType!);
}
return diagnostics.wrap(sdkType);
}
}
return diagnostics.wrap(undefined);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -91,3 +91,48 @@ it("alias of EmbeddingVector", async () => {
strictEqual(property.type.crossLanguageDefinitionId, "Azure.Core.EmbeddingVector");
strictEqual(property.type.valueType.kind, "int32");
});

it("same type's array come to same type", async () => {
await runner.compile(`
@service
namespace TestClient {
model Test {
prop: string;
}

model TestArray {
prop1: Test[];
prop2: Test[];
prop3: string[];
prop4: string[];
prop5: Test[][];
prop6: Test[][];
prop7: Record<Test>[];
prop8: Record<Test>[];
prop9: Record<Record<Test>>[];
prop10: Record<Record<Test>>[];
}

op get(): TestArray;
}
`);
const testArrayModel = runner.context.sdkPackage.models[0];
strictEqual(testArrayModel.kind, "model");
strictEqual(testArrayModel.name, "TestArray");
strictEqual(testArrayModel.properties.length, 10);
const prop1 = testArrayModel.properties[0];
const prop2 = testArrayModel.properties[1];
const prop3 = testArrayModel.properties[2];
const prop4 = testArrayModel.properties[3];
const prop5 = testArrayModel.properties[4];
const prop6 = testArrayModel.properties[5];
const prop7 = testArrayModel.properties[6];
const prop8 = testArrayModel.properties[7];
const prop9 = testArrayModel.properties[8];
const prop10 = testArrayModel.properties[9];
strictEqual(prop1.type, prop2.type);
strictEqual(prop3.type, prop4.type);
strictEqual(prop5.type, prop6.type);
strictEqual(prop7.type, prop8.type);
strictEqual(prop9.type, prop10.type);
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { strictEqual } from "assert";
import { beforeEach, it } from "vitest";
import { SdkTestRunner, createSdkTestRunner } from "../test-host.js";

let runner: SdkTestRunner;

beforeEach(async () => {
runner = await createSdkTestRunner({ emitterName: "@azure-tools/typespec-java" });
});

it("same type's dictionary come to same type", async () => {
await runner.compile(`
@service
namespace TestClient {
model Test {
prop: string;
}

model TestDictionary {
prop1: Record<Test>;
prop2: Record<Test>;
prop3: Record<string>;
prop4: Record<string>;
prop5: Record<Test[]>;
prop6: Record<Test[]>;
prop7: Record<Record<Test>>;
prop8: Record<Record<Test>>;
prop9: Record<Test[][]>;
prop10: Record<Test[][]>;
}
op get(): TestDictionary;
}
`);
const testArrayModel = runner.context.sdkPackage.models[0];
strictEqual(testArrayModel.kind, "model");
strictEqual(testArrayModel.name, "TestDictionary");
strictEqual(testArrayModel.properties.length, 10);
const prop1 = testArrayModel.properties[0];
const prop2 = testArrayModel.properties[1];
const prop3 = testArrayModel.properties[2];
const prop4 = testArrayModel.properties[3];
const prop5 = testArrayModel.properties[4];
const prop6 = testArrayModel.properties[5];
const prop7 = testArrayModel.properties[6];
const prop8 = testArrayModel.properties[7];
const prop9 = testArrayModel.properties[8];
const prop10 = testArrayModel.properties[9];
strictEqual(prop1.type, prop2.type);
strictEqual(prop3.type, prop4.type);
strictEqual(prop5.type, prop6.type);
strictEqual(prop7.type, prop8.type);
strictEqual(prop9.type, prop10.type);
});
Loading