Skip to content
Open
Show file tree
Hide file tree
Changes from 5 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
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
changeKind: fix
packages:
- "@azure-tools/typespec-client-generator-core"
---

Generate names for anonymous models in LroMetadata.
22 changes: 22 additions & 0 deletions packages/typespec-client-generator-core/src/public-utils.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { getLroMetadata } from "@azure-tools/typespec-azure-core";
import {
Diagnostic,
Enum,
Expand Down Expand Up @@ -503,6 +504,27 @@ function getContextPath(
}
}

const lroMetadata = getLroMetadata(context.program, root);
if (lroMetadata) {
const anonymousCandidates = [
{ value: lroMetadata.finalResult, label: "FinalResult" },
{ value: lroMetadata.logicalResult, label: "LogicalResult" },
{ value: lroMetadata.envelopeResult, label: "EnvelopeResult" },
{ value: lroMetadata.finalEnvelopeResult, label: "FinalEnvelopeResult" },
];

for (const { value, label } of anonymousCandidates) {
if (!value || value === "void") {
continue;
}
visited.clear();
result = [{ name: root.name, type: root }];
if (dfsModelProperties(typeToFind, value, label)) {
return result;
}
}
}

const overriddenClientMethod = getOverriddenClientMethod(context, root);
visited.clear();
result = [{ name: root.name, type: root }];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -816,6 +816,97 @@ describe("data plane LRO templates", () => {
);
strictEqual(lroMetadata.pollingStep.responseBody, analyzeOperationModel);
});

// https://github.com/Azure/typespec-azure/issues/2325
it("LroMetadata final result anonymous model", async () => {
await runner.compileWithVersionedService(`
alias ServiceTraits = NoRepeatableRequests &
NoConditionalRequests &
NoClientRequestId;

alias apiOperations = Azure.Core.ResourceOperations<ServiceTraits>;

@doc("Create an instruction.")
@pollingOperation(
OperationProgress.getOperationResult
)
op update is apiOperations.LongRunningResourceCreateOrUpdate<Instruction>;

@resource("instruction")
model Instruction {
@key
@visibility(Lifecycle.Read)
id: string;

@visibility(Lifecycle.Update)
instructionId: string;
}

@doc("Operation Status")
@lroStatus
union OperationStatusValue {
Copy link
Member

@weidongxu-microsoft weidongxu-microsoft Dec 5, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it needed to be defined for this test? Can we use the Foundations.OperationState in Core?
Or OperationResultQuery itself can be replaced by some model in Core?

Feel that both of them is not relevant to the test case. Only the definition of Instruction model matters.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, let me try.

Copy link
Contributor Author

@XiaofeiCao XiaofeiCao Dec 5, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After using the standard ResourceOperationStatus, there seems no anonymous any more:
image

See playground.

I seem to get why the anonymous model is generated.. It seems to me that it's trying generate a model to conform to the standard ResourceOperationStatus...
Ideally, when this model is generated, it should be given a name by compiler. Guess a protection here is acceptable as well.

@doc("The operation is Accepted.")
Accepted: "Accepted",

@doc("The operation is InProgress.")
InProgress: "InProgress",

@doc("The operation is Succeeded.")
Succeeded: "Succeeded",

@doc("The operation is Failed.")
Failed: "Failed",

@doc("The operation is Canceled.")
Canceled: "Canceled",

string,
}

@doc("Operation Response Model")
@resource("operation")
model OperationResultQuery {
@doc("The operation status.")
@visibility(Lifecycle.Read)
status: OperationStatusValue;

@doc("The operation id.")
@key("operationId")
@visibility(Lifecycle.Read)
operationId: string;

@doc("The error message.")
@visibility(Lifecycle.Read)
errorMessage: string[];
}

@doc("Get operation progress")
interface OperationProgress {
@doc("Get operation progress")
getOperationResult is apiOperations.ResourceRead<OperationResultQuery>;
}
`);
const method = runner.context.sdkPackage.clients[0].methods.find((m) => m.name === "update");
assert.exists(method);
strictEqual(method.kind, "lro");
const response = method.response.type;
assert.strictEqual(response?.kind, "model");
if (!response || response.kind !== "model") {
assert.fail("Expected final response to be a model.");
}
assert.isTrue(response.isGeneratedName);
const generatedName = response.name;
assert.strictEqual("UpdateFinalResult", generatedName);
const crossLanguageId = response.crossLanguageDefinitionId;
assert.isFalse(crossLanguageId.includes(".."));
const lroMetadata = method.lroMetadata;
assert.exists(lroMetadata);
const finalResponse = lroMetadata.finalResponse;
assert.exists(finalResponse);
const finalResult = finalResponse.result;
assert.exists(finalResult);
assert.strictEqual(finalResult.name, "UpdateFinalResult");
});
});

describe("Arm LRO templates", () => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
import { AzureCoreTestLibrary } from "@azure-tools/typespec-azure-core/testing";
import { AzureResourceManagerTestLibrary } from "@azure-tools/typespec-azure-resource-manager/testing";
import {
BasicTestRunner,
createLinterRuleTester,
LinterRuleTester,
} from "@typespec/compiler/testing";
import { OpenAPITestLibrary } from "@typespec/openapi/testing";
import { beforeEach, describe, it } from "vitest";
import { noUnnamedTypesRule } from "../../src/rules/no-unnamed-types.rule.js";
import { createSdkTestRunner } from "../test-host.js";
Expand All @@ -11,7 +14,10 @@ let runner: BasicTestRunner;
let tester: LinterRuleTester;

beforeEach(async () => {
runner = await createSdkTestRunner();
runner = await createSdkTestRunner({
librariesToAdd: [AzureCoreTestLibrary, OpenAPITestLibrary],
autoImports: ["@azure-tools/typespec-azure-core"],
});
tester = createLinterRuleTester(
runner,
noUnnamedTypesRule,
Expand Down Expand Up @@ -233,6 +239,49 @@ describe("models", () => {
)
.toBeValid();
});

it("anonymous model caused by lro metadata", async () => {
const armRunner = await createSdkTestRunner({
librariesToAdd: [AzureResourceManagerTestLibrary, AzureCoreTestLibrary, OpenAPITestLibrary],
autoImports: ["@azure-tools/typespec-azure-resource-manager"],
autoUsings: ["Azure.ResourceManager", "Azure.Core", "Azure.Core.Traits"],
});
const armTester = createLinterRuleTester(
armRunner,
noUnnamedTypesRule,
"@azure-tools/typespec-client-generator-core",
);
await armTester
.expect(
`
@armProviderNamespace
@service
@versioned(Versions)
namespace TestClient;
enum Versions {
@armCommonTypesVersion(Azure.ResourceManager.CommonTypes.Versions.v5)
v1: "v1",
}
model Employee is TrackedResource<EmployeeProperties> {
...ResourceNameParameter<Employee>;
}
model MoveRequest {
targetResourceGroup?: string;
}
model EmployeeProperties {
age?: int32;
}
op move is ArmResourceActionAsync<Employee, MoveRequest, {@body body: {id?: string}}>;
`,
)
.toEmitDiagnostics([
{
code: "@azure-tools/typespec-client-generator-core/no-unnamed-types",
severity: "warning",
message: `Anonymous model with generated name "MoveFinalResult" detected. Define this model separately with a proper name to improve code readability and reusability.`,
},
]);
});
});

describe("unions", () => {
Expand Down
Loading