From d2d665bb160a96c384daee82d91b264f98368087 Mon Sep 17 00:00:00 2001 From: Yuchao Yan Date: Fri, 9 Oct 2026 13:25:05 +0800 Subject: [PATCH 1/8] Promote lro-missing-polling-metadata ARM lint rule Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 625c0cd9-bacc-4f9c-8b96-4ded1932a30a --- .../lro-missing-polling-metadata-arm.md | 7 + .../lro-missing-polling-metadata-rulesets.md | 7 + .../typespec-azure-resource-manager/README.md | 1 + .../src/linter.ts | 2 + .../src/rules/lro-missing-polling-metadata.md | 94 +++++++ .../src/rules/lro-missing-polling-metadata.ts | 42 +++ .../lro-missing-polling-metadata.test.ts | 262 ++++++++++++++++++ .../src/rulesets/resource-manager.ts | 1 + .../content/docs/docs/howtos/ARM/arm-rules.md | 1 + .../reference/linter.md | 1 + 10 files changed, 418 insertions(+) create mode 100644 .chronus/changes/lro-missing-polling-metadata-arm.md create mode 100644 .chronus/changes/lro-missing-polling-metadata-rulesets.md create mode 100644 packages/typespec-azure-resource-manager/src/rules/lro-missing-polling-metadata.md create mode 100644 packages/typespec-azure-resource-manager/src/rules/lro-missing-polling-metadata.ts create mode 100644 packages/typespec-azure-resource-manager/test/rules/lro-missing-polling-metadata.test.ts diff --git a/.chronus/changes/lro-missing-polling-metadata-arm.md b/.chronus/changes/lro-missing-polling-metadata-arm.md new file mode 100644 index 0000000000..6d2aedf824 --- /dev/null +++ b/.chronus/changes/lro-missing-polling-metadata-arm.md @@ -0,0 +1,7 @@ +--- +changeKind: feature +packages: + - "@azure-tools/typespec-azure-resource-manager" +--- + +Add the opt-in `lro-missing-polling-metadata` lint rule for ARM PATCH and provider/collection POST operations returning `202` without native polling metadata. Use asynchronous ARM templates or preserve supported semantic LRO headers. diff --git a/.chronus/changes/lro-missing-polling-metadata-rulesets.md b/.chronus/changes/lro-missing-polling-metadata-rulesets.md new file mode 100644 index 0000000000..b9b6b995bd --- /dev/null +++ b/.chronus/changes/lro-missing-polling-metadata-rulesets.md @@ -0,0 +1,7 @@ +--- +changeKind: internal +packages: + - "@azure-tools/typespec-azure-rulesets" +--- + +Register the ARM `lro-missing-polling-metadata` lint rule as disabled in the resource-manager ruleset. diff --git a/packages/typespec-azure-resource-manager/README.md b/packages/typespec-azure-resource-manager/README.md index 45af72fa73..000296ae96 100644 --- a/packages/typespec-azure-resource-manager/README.md +++ b/packages/typespec-azure-resource-manager/README.md @@ -65,6 +65,7 @@ Available ruleSets: | [`@azure-tools/typespec-azure-resource-manager/arm-resource-invalid-action-verb`](https://azure.github.io/typespec-azure/docs/libraries/azure-resource-manager/rules/arm-resource-invalid-action-verb) | Actions must be HTTP Post or Get operations. | | [`@azure-tools/typespec-azure-resource-manager/improper-subscription-list-operation`](https://azure.github.io/typespec-azure/docs/libraries/azure-resource-manager/rules/improper-subscription-list-operation) | Tenant and Extension resources should not define a list by subscription operation. | | [`@azure-tools/typespec-azure-resource-manager/lro-location-header`](https://azure.github.io/typespec-azure/docs/libraries/azure-resource-manager/rules/lro-location-header) | A 202 response should include a Location response header. | +| [`@azure-tools/typespec-azure-resource-manager/lro-missing-polling-metadata`](https://azure.github.io/typespec-azure/docs/libraries/azure-resource-manager/rules/lro-missing-polling-metadata) | ARM PATCH and provider/collection POST operations returning 202 must have native polling metadata. | | [`@azure-tools/typespec-azure-resource-manager/list-operation-missing-pageable`](https://azure.github.io/typespec-azure/docs/libraries/azure-resource-manager/rules/list-operation-missing-pageable) | ARM GET operations on list paths must define TypeSpec paging metadata. | | [`@azure-tools/typespec-azure-resource-manager/missing-x-ms-identifiers`](https://azure.github.io/typespec-azure/docs/libraries/azure-resource-manager/rules/missing-x-ms-identifiers) | Array properties should describe their identifying properties with x-ms-identifiers. Decorate the property with @OpenAPI.extension("x-ms-identifiers", #[id-prop]) where "id-prop" is a list of the names of identifying properties in the item type. | | [`@azure-tools/typespec-azure-resource-manager/no-response-body`](https://azure.github.io/typespec-azure/docs/libraries/azure-resource-manager/rules/no-response-body) | Check that the body is empty for 202 and 204 responses, and not empty for other success (2xx) responses. | diff --git a/packages/typespec-azure-resource-manager/src/linter.ts b/packages/typespec-azure-resource-manager/src/linter.ts index 4333e5357a..c3283283c3 100644 --- a/packages/typespec-azure-resource-manager/src/linter.ts +++ b/packages/typespec-azure-resource-manager/src/linter.ts @@ -28,6 +28,7 @@ import { improperSubscriptionListOperationRule } from "./rules/improper-subscrip import { listOperationMissingPageableRule } from "./rules/list-operation-missing-pageable.js"; import { listResponseEnvelopeRule } from "./rules/list-response-envelope.js"; import { lroLocationHeaderRule } from "./rules/lro-location-header.js"; +import { lroMissingPollingMetadataRule } from "./rules/lro-missing-polling-metadata.js"; import { lroResponseMismatchRule } from "./rules/lro-response-mismatch.js"; import { missingXmsIdentifiersRule } from "./rules/missing-x-ms-identifiers.js"; import { noEmptyModel } from "./rules/no-empty-model.js"; @@ -91,6 +92,7 @@ const rules = [ armResourceInvalidActionVerbRule, improperSubscriptionListOperationRule, lroLocationHeaderRule, + lroMissingPollingMetadataRule, listOperationMissingPageableRule, missingXmsIdentifiersRule, noResponseBodyRule, diff --git a/packages/typespec-azure-resource-manager/src/rules/lro-missing-polling-metadata.md b/packages/typespec-azure-resource-manager/src/rules/lro-missing-polling-metadata.md new file mode 100644 index 0000000000..ff28d3540c --- /dev/null +++ b/packages/typespec-azure-resource-manager/src/rules/lro-missing-polling-metadata.md @@ -0,0 +1,94 @@ +ARM PATCH and provider or collection POST operations returning `202 Accepted` +must describe how clients poll the operation and obtain its final result. +A plain HTTP `Location` header does not supply that polling metadata. + +Use standard asynchronous ARM templates, and preserve their semantic LRO +headers when customizing responses. For example, retain +`ArmLroLocationHeader` in a custom PATCH response. +Synchronous templates can also supply polling metadata through a customized +accepted response, but changing only the status code and header name is not +sufficient. + +This rule checks PATCH and POST operations marked as ARM provider or collection +actions. It reports once on each operation with an explicit `202` response and +no native LRO metadata. Operations without `202` or with native polling metadata +are compliant. Registered resource-instance POST, PUT, and DELETE response +requirements are covered by the existing ARM response-code rules. + +The rule is available but disabled by default in the resource-manager ruleset. + +## Impact + +- **Area:** API, SDK + +Missing polling metadata prevents Azure tooling and generated clients from +describing the operation's polling and final-result behavior. Clients may treat +the initial accepted response as the final result instead of waiting for +completion. + +## ❌ Incorrect + +Replacing the standard headers with a string-valued `Location` header preserves +the accepted response but removes its polling semantics: + +```tsp +@armProviderNamespace +namespace Microsoft.Contoso; + +model Widget is TrackedResource<{}> { + ...ResourceNameParameter; +} + +model PlainLocationHeaders { + ...Azure.Core.Foundations.RetryAfterHeader; + @header("Location") location?: string; +} + +@armResourceOperations +interface Widgets { + update is ArmCustomPatchAsync; +} +``` + +## ✅ Correct + +Keep the same asynchronous operation and accepted response, but restore the +supported polling and final-result metadata: + +```tsp +@armProviderNamespace +namespace Microsoft.Contoso; + +model Widget is TrackedResource<{}> { + ...ResourceNameParameter; +} + +@armResourceOperations +interface Widgets { + update is ArmCustomPatchAsync< + Widget, + Widget, + LroHeaders = ArmLroLocationHeader & + Azure.Core.Foundations.RetryAfterHeader + >; +} +``` + +Custom `ArmProviderActionAsync` headers have the same requirement. The default +asynchronous provider template already carries supported polling metadata. + +## Suppression + +Prefer restoring the semantic headers or using the standard asynchronous +template. Suppress only when an externally defined polling contract cannot yet +be represented using supported TypeSpec metadata. Document that contract and +why a temporary exception is necessary. + +Place +`#suppress "@azure-tools/typespec-azure-resource-manager/lro-missing-polling-metadata" ""` +on the affected operation. + +## LintDiff Equivalent + +This rule originates from +[LroExtension](https://github.com/Azure/azure-openapi-validator/blob/main/docs/lro-extension.md). diff --git a/packages/typespec-azure-resource-manager/src/rules/lro-missing-polling-metadata.ts b/packages/typespec-azure-resource-manager/src/rules/lro-missing-polling-metadata.ts new file mode 100644 index 0000000000..f311efc5ba --- /dev/null +++ b/packages/typespec-azure-resource-manager/src/rules/lro-missing-polling-metadata.ts @@ -0,0 +1,42 @@ +import { getLroMetadata } from "@azure-tools/typespec-azure-core"; +import { createRule, fileRef, listServices, type Operation } from "@typespec/compiler"; +import { getHttpService } from "@typespec/http"; +import { isArmCollectionAction } from "../operations.js"; + +export const lroMissingPollingMetadataRule = createRule({ + name: "lro-missing-polling-metadata", + docs: fileRef.fromPackageRoot("src/rules/lro-missing-polling-metadata.md"), + description: + "ARM PATCH and provider/collection POST operations returning 202 must have native polling metadata.", + severity: "warning", + url: "https://azure.github.io/typespec-azure/docs/libraries/azure-resource-manager/rules/lro-missing-polling-metadata", + messages: { + default: + "ARM operations returning 202 must have native polling metadata. Use an asynchronous ARM template or preserve supported semantic LRO headers.", + }, + create(context) { + return { + root() { + const reportedOperations = new Set(); + for (const service of listServices(context.program)) { + const [httpService] = getHttpService(context.program, service.type); + for (const httpOperation of httpService.operations) { + const operation = httpOperation.operation; + const uncovered = + httpOperation.verb === "patch" || + (httpOperation.verb === "post" && isArmCollectionAction(context.program, operation)); + if ( + uncovered && + httpOperation.responses.some((response) => response.statusCodes === 202) && + getLroMetadata(context.program, operation) === undefined && + !reportedOperations.has(operation) + ) { + reportedOperations.add(operation); + context.reportDiagnostic({ target: operation }); + } + } + } + }, + }; + }, +}); diff --git a/packages/typespec-azure-resource-manager/test/rules/lro-missing-polling-metadata.test.ts b/packages/typespec-azure-resource-manager/test/rules/lro-missing-polling-metadata.test.ts new file mode 100644 index 0000000000..9df40f1f6e --- /dev/null +++ b/packages/typespec-azure-resource-manager/test/rules/lro-missing-polling-metadata.test.ts @@ -0,0 +1,262 @@ +import { Tester } from "#test/tester.js"; +import { getSourceLocation } from "@typespec/compiler"; +import { createLinterRuleTester, type LinterRuleTester } from "@typespec/compiler/testing"; +import { readFileSync } from "node:fs"; +import { beforeEach, expect, it, vi } from "vitest"; +import { lroMissingPollingMetadataRule } from "../../src/rules/lro-missing-polling-metadata.js"; + +vi.mock("@azure-tools/typespec-autorest", () => { + throw new Error("Native lint must not load the AutoRest emitter."); +}); +vi.mock("@azure-tools/typespec-client-generator-core", () => { + throw new Error("ARM lint must not load TCGC."); +}); + +const header = ` +@armProviderNamespace +@service +@versioned(Versions) +namespace Microsoft.TestService; +enum Versions { + @useDependency(Azure.ResourceManager.CommonTypes.Versions.v5) + @armCommonTypesVersion(Azure.ResourceManager.CommonTypes.Versions.v3) + v2024_01_01: "2024-01-01", +} +model Widget is TrackedResource { + @key("widgetName") @segment("widgets") + @pattern("^[a-zA-Z0-9-]{3,24}$") @path + name: string; +} +model WidgetProperties { + description?: string; + @visibility(Lifecycle.Read) + provisioningState?: ProvisioningState; +} +union ProvisioningState { string, ResourceProvisioningState } +model PlainLocationHeaders { + ...Azure.Core.Foundations.RetryAfterHeader; + @header("Location") location?: string; +} +interface Operations extends Azure.ResourceManager.Operations {} +`; +const diagnostic = { + code: "@azure-tools/typespec-azure-resource-manager/lro-missing-polling-metadata", + message: lroMissingPollingMetadataRule.messages.default, +}; +const documentation = readFileSync( + new URL("../../src/rules/lro-missing-polling-metadata.md", import.meta.url), + "utf8", +).replace(/\r\n/g, "\n"); +const examples = Array.from( + documentation.matchAll(/```tsp\n([\s\S]*?)\n```/g), + (match) => match[1], +); +let tester: LinterRuleTester; + +beforeEach(async () => { + tester = createLinterRuleTester( + await Tester.createInstance(), + lroMissingPollingMetadataRule, + "@azure-tools/typespec-azure-resource-manager", + ); +}); + +async function expectViolation(operation: string, interfaceName: string) { + await tester + .expect( + `${header} + @armResourceOperations + interface ${interfaceName} { /*target*/${operation} } + `, + ) + .toEmitDiagnostics(({ target }) => ({ + ...diagnostic, + pos: getSourceLocation(target).pos, + end: getSourceLocation(target).end, + })); +} + +async function expectCompliant(operation: string, interfaceName: string) { + await tester + .expect(`${header} @armResourceOperations interface ${interfaceName} { ${operation} }`) + .toBeValid(); +} + +it("reports a synchronous custom PATCH returning 202 without polling metadata", async () => { + await expectViolation( + "update is ArmCustomPatchSync | ArmAcceptedResponse>;", + "Widgets", + ); +}); + +it("reports the documented async PATCH customization without polling metadata", async () => { + expect(examples).toHaveLength(2); + await tester + .expect(examples[0].replace("update is", "/*target*/update is")) + .toEmitDiagnostics(({ target }) => ({ + ...diagnostic, + pos: getSourceLocation(target).pos, + end: getSourceLocation(target).end, + })); +}); + +it("reports a synchronous provider POST returning 202 without polling metadata", async () => { + await expectViolation( + "startProvider is ArmProviderActionSync>;", + "ProviderOperations", + ); +}); + +it("reports an async provider POST with plain Location headers", async () => { + await expectViolation( + "startProvider is ArmProviderActionAsync;", + "ProviderOperations", + ); +}); + +it("accepts standard async resource PATCH polling metadata", async () => { + await expectCompliant("update is ArmResourcePatchAsync;", "Widgets"); +}); + +it("accepts default async custom PATCH polling metadata", async () => { + await expectCompliant("update is ArmCustomPatchAsync;", "Widgets"); +}); + +it("accepts the documented async PATCH with semantic LRO headers", async () => { + expect(examples).toHaveLength(2); + await tester.expect(examples[1]).toBeValid(); +}); + +it("accepts synchronous custom PATCH without a 202 response", async () => { + await expectCompliant("update is ArmCustomPatchSync;", "Widgets"); +}); + +it("accepts synchronous custom PATCH with semantic accepted-response headers", async () => { + await expectCompliant( + "update is ArmCustomPatchSync | ArmAcceptedResponse>>;", + "Widgets", + ); +}); + +it("accepts default async provider polling metadata", async () => { + await expectCompliant( + "startProvider is ArmProviderActionAsync;", + "ProviderOperations", + ); +}); + +it("accepts synchronous provider actions without a 202 response", async () => { + await expectCompliant( + "startProvider is ArmProviderActionSync;", + "ProviderOperations", + ); +}); + +it("accepts synchronous provider actions with semantic accepted-response headers", async () => { + await expectCompliant( + "startProvider is ArmProviderActionSync>;", + "ProviderOperations", + ); +}); + +it("reports a nested service operation once across overlapping HTTP service traversals", async () => { + await tester + .expect( + `${header.replace( + "interface Operations extends", + '@route("/parent") interface Operations extends', + )} + @armProviderNamespace("Microsoft.Nested") + @service + @versioned(Versions) + namespace Nested { + @route("/child") + interface Operations extends Azure.ResourceManager.Operations {} + @armResourceOperations + interface ProviderOperations { + /*target*/startProvider is ArmProviderActionAsync< + Request = void, Response = void, LroHeaders = PlainLocationHeaders + >; + } + } + `, + ) + .toEmitDiagnostics(({ target }) => ({ + ...diagnostic, + pos: getSourceLocation(target).pos, + end: getSourceLocation(target).end, + })); +}); + +it("reports each distinct provider operation once when response headers are shared", async () => { + await tester + .expect( + `${header} + @armResourceOperations + interface ProviderOperations { + /*start*/startProvider is ArmProviderActionAsync< + Request = void, Response = void, LroHeaders = PlainLocationHeaders + >; + /*stop*/stopProvider is ArmProviderActionAsync< + Request = void, Response = void, LroHeaders = PlainLocationHeaders + >; + } + `, + ) + .toEmitDiagnostics(({ start, stop }) => + [start, stop].map((operation) => ({ + ...diagnostic, + pos: getSourceLocation(operation).pos, + end: getSourceLocation(operation).end, + })), + ); +}); + +it("checks PATCH without provider metadata when the ARM rule is selected", async () => { + await tester + .expect( + ` + @service namespace SelectedArmProgram; + model Accepted { @statusCode statusCode: 202; @header("Location") location?: string; } + @patch @route("/widgets") op /*target*/update(): Accepted; + `, + ) + .toEmitDiagnostics(({ target }) => ({ + ...diagnostic, + pos: getSourceLocation(target).pos, + end: getSourceLocation(target).end, + })); +}); + +it("leaves registered resource-instance POST response-code ownership to the existing rule", async () => { + await expectCompliant( + "startWidget is ArmResourceActionSync>;", + "Widgets", + ); +}); + +it("ignores GET PUT and DELETE accepted responses", async () => { + await tester + .expect( + ` + @service namespace SelectedArmProgram; + model Accepted { @statusCode statusCode: 202; @header("Location") location?: string; } + @get @route("/get") op read(): Accepted; + @put @route("/put") op create(): Accepted; + @delete @route("/delete") op remove(): Accepted; + `, + ) + .toBeValid(); +}); + +it("ignores unmarked POST accepted responses", async () => { + await tester + .expect( + ` + @service namespace SelectedArmProgram; + model Accepted { @statusCode statusCode: 202; @header("Location") location?: string; } + @post @route("/action") op act(): Accepted; + `, + ) + .toBeValid(); +}); diff --git a/packages/typespec-azure-rulesets/src/rulesets/resource-manager.ts b/packages/typespec-azure-rulesets/src/rulesets/resource-manager.ts index b04bddedb8..bde8c11d42 100644 --- a/packages/typespec-azure-rulesets/src/rulesets/resource-manager.ts +++ b/packages/typespec-azure-rulesets/src/rulesets/resource-manager.ts @@ -97,6 +97,7 @@ export default { "@azure-tools/typespec-azure-resource-manager/no-tenant-level-apis": false, "@azure-tools/typespec-azure-resource-manager/list-operation-missing-pageable": false, "@azure-tools/typespec-azure-resource-manager/lro-location-header": true, + "@azure-tools/typespec-azure-resource-manager/lro-missing-polling-metadata": false, "@azure-tools/typespec-azure-resource-manager/missing-x-ms-identifiers": true, "@azure-tools/typespec-azure-resource-manager/no-response-body": true, "@azure-tools/typespec-azure-resource-manager/no-query-in-post": false, diff --git a/website/src/content/docs/docs/howtos/ARM/arm-rules.md b/website/src/content/docs/docs/howtos/ARM/arm-rules.md index 0f8253d4d2..a65b6858e6 100644 --- a/website/src/content/docs/docs/howtos/ARM/arm-rules.md +++ b/website/src/content/docs/docs/howtos/ARM/arm-rules.md @@ -124,6 +124,7 @@ The tables below provide guidance to rule authors and ARM reviewers on how to ev | [`list-operation-missing-pageable`](https://azure.github.io/typespec-azure/docs/libraries/azure-resource-manager/rules/list-operation-missing-pageable/) | [`XmsPageableForListCalls`](https://github.com/Azure/azure-openapi-validator/blob/main/docs/xms-pageable-for-list-calls.md) | **API, SDK.** Missing paging metadata makes generated list methods inconsistent and makes adding continuation links a breaking change. | | [`list-response-envelope`](https://azure.github.io/typespec-azure/docs/libraries/azure-resource-manager/rules/list-response-envelope/) | [`GetCollectionOnlyHasValueAndNextLink`](https://github.com/Azure/azure-openapi-validator/blob/main/packages/rulesets/src/spectral/functions/get-collection-only-has-value-and-next-link.ts) | **API, SDK.** A nonstandard collection envelope prevents consistent pagination behavior. | | [`lro-location-header`](https://azure.github.io/typespec-azure/docs/libraries/azure-resource-manager/rules/lro-location-header/) | [`LroLocationHeader`](https://github.com/Azure/azure-openapi-validator/blob/main/docs/lro-location-header.md) | **API.** RPC violation. | +| [`lro-missing-polling-metadata`](https://azure.github.io/typespec-azure/docs/libraries/azure-resource-manager/rules/lro-missing-polling-metadata/) | [`LroExtension`](https://github.com/Azure/azure-openapi-validator/blob/main/docs/lro-extension.md) (partial) | **API, SDK.** ARM PATCH and provider/collection POST responses with `202` need native polling metadata to describe polling and final results. Disabled by default. | | [`lro-response-mismatch`](https://azure.github.io/typespec-azure/docs/libraries/azure-resource-manager/rules/lro-response-mismatch/) | — | **API, SDK.** A final result that differs from the operation response can break generated long-running operation methods. | | [`missing-operations-endpoint`](https://azure.github.io/typespec-azure/docs/libraries/azure-resource-manager/rules/missing-operations-endpoint/) | [R3023](https://github.com/Azure/azure-rest-api-specs/blob/main/documentation/openapi-authoring-automated-guidelines.md#r3023) | **API.** RPC violation. | | [`missing-x-ms-identifiers`](https://azure.github.io/typespec-azure/docs/libraries/azure-resource-manager/rules/missing-x-ms-identifiers/) | [R4041](https://github.com/Azure/azure-rest-api-specs/blob/main/documentation/openapi-authoring-automated-guidelines.md#r4041) (warning) | **API.** No real impact (an old pattern). | diff --git a/website/src/content/docs/docs/libraries/azure-resource-manager/reference/linter.md b/website/src/content/docs/docs/libraries/azure-resource-manager/reference/linter.md index 7371c440d3..21afb21209 100644 --- a/website/src/content/docs/docs/libraries/azure-resource-manager/reference/linter.md +++ b/website/src/content/docs/docs/libraries/azure-resource-manager/reference/linter.md @@ -59,6 +59,7 @@ Available ruleSets: | [`@azure-tools/typespec-azure-resource-manager/arm-resource-invalid-action-verb`](../rules/arm-resource-invalid-action-verb.md) | Actions must be HTTP Post or Get operations. | | [`@azure-tools/typespec-azure-resource-manager/improper-subscription-list-operation`](../rules/improper-subscription-list-operation.md) | Tenant and Extension resources should not define a list by subscription operation. | | [`@azure-tools/typespec-azure-resource-manager/lro-location-header`](../rules/lro-location-header.md) | A 202 response should include a Location response header. | +| [`@azure-tools/typespec-azure-resource-manager/lro-missing-polling-metadata`](../rules/lro-missing-polling-metadata.md) | ARM PATCH and provider/collection POST operations returning 202 must have native polling metadata. | | [`@azure-tools/typespec-azure-resource-manager/list-operation-missing-pageable`](../rules/list-operation-missing-pageable.md) | ARM GET operations on list paths must define TypeSpec paging metadata. | | [`@azure-tools/typespec-azure-resource-manager/missing-x-ms-identifiers`](../rules/missing-x-ms-identifiers.md) | Array properties should describe their identifying properties with x-ms-identifiers. Decorate the property with @OpenAPI.extension("x-ms-identifiers", #[id-prop]) where "id-prop" is a list of the names of identifying properties in the item type. | | [`@azure-tools/typespec-azure-resource-manager/no-response-body`](../rules/no-response-body.md) | Check that the body is empty for 202 and 204 responses, and not empty for other success (2xx) responses. | From 98519f936a2838b5c95620ce4f449c08d20578a5 Mon Sep 17 00:00:00 2001 From: Yuchao Yan Date: Fri, 9 Oct 2026 18:21:24 +0800 Subject: [PATCH 2/8] Fix LRO metadata visitation before resolution Mirror the reviewed LroExtension source repair and consolidate the registered regression while preserving fresh per-test setup. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 625c0cd9-bacc-4f9c-8b96-4ded1932a30a --- .../lro-missing-polling-metadata-arm.md | 2 + .../src/rules/lro-missing-polling-metadata.md | 11 ++++ .../src/rules/lro-missing-polling-metadata.ts | 12 ++-- .../lro-missing-polling-metadata.test.ts | 65 ++++++++++++++++++- 4 files changed, 83 insertions(+), 7 deletions(-) diff --git a/.chronus/changes/lro-missing-polling-metadata-arm.md b/.chronus/changes/lro-missing-polling-metadata-arm.md index 6d2aedf824..208fe168d4 100644 --- a/.chronus/changes/lro-missing-polling-metadata-arm.md +++ b/.chronus/changes/lro-missing-polling-metadata-arm.md @@ -5,3 +5,5 @@ packages: --- Add the opt-in `lro-missing-polling-metadata` lint rule for ARM PATCH and provider/collection POST operations returning `202` without native polling metadata. Use asynchronous ARM templates or preserve supported semantic LRO headers. + +Check each eligible operation once across overlapping service declarations, including compliant operations, to avoid repeated native metadata diagnostics. diff --git a/packages/typespec-azure-resource-manager/src/rules/lro-missing-polling-metadata.md b/packages/typespec-azure-resource-manager/src/rules/lro-missing-polling-metadata.md index ff28d3540c..ba6b9956a6 100644 --- a/packages/typespec-azure-resource-manager/src/rules/lro-missing-polling-metadata.md +++ b/packages/typespec-azure-resource-manager/src/rules/lro-missing-polling-metadata.md @@ -15,6 +15,10 @@ no native LRO metadata. Operations without `202` or with native polling metadata are compliant. Registered resource-instance POST, PUT, and DELETE response requirements are covered by the existing ARM response-code rules. +Operations reached through overlapping service declarations are checked once, +including compliant operations. This avoids repeating diagnostics from native +polling-metadata resolution. + The rule is available but disabled by default in the resource-manager ruleset. ## Impact @@ -92,3 +96,10 @@ on the affected operation. This rule originates from [LroExtension](https://github.com/Azure/azure-openapi-validator/blob/main/docs/lro-extension.md). + +The native implementation follows the reviewed +[lintdiff source rule](https://github.com/Azure/typespec-azure/blob/feature/lintdiff-lro-extension/packages/typespec-lintdiff/src/rules/lro-extension.ts) +at `9afb5818fba089835296258049ad7104e8a92c50`, refreshed from +`f0973f43bffa73169fdde0436566c2534bdab595` to visit eligible operations before +resolving polling metadata. The official ARM rule preserves the destination's +providerless applicability adaptation and remains disabled by default. diff --git a/packages/typespec-azure-resource-manager/src/rules/lro-missing-polling-metadata.ts b/packages/typespec-azure-resource-manager/src/rules/lro-missing-polling-metadata.ts index f311efc5ba..6257692cb4 100644 --- a/packages/typespec-azure-resource-manager/src/rules/lro-missing-polling-metadata.ts +++ b/packages/typespec-azure-resource-manager/src/rules/lro-missing-polling-metadata.ts @@ -17,7 +17,7 @@ export const lroMissingPollingMetadataRule = createRule({ create(context) { return { root() { - const reportedOperations = new Set(); + const visitedOperations = new Set(); for (const service of listServices(context.program)) { const [httpService] = getHttpService(context.program, service.type); for (const httpOperation of httpService.operations) { @@ -28,11 +28,13 @@ export const lroMissingPollingMetadataRule = createRule({ if ( uncovered && httpOperation.responses.some((response) => response.statusCodes === 202) && - getLroMetadata(context.program, operation) === undefined && - !reportedOperations.has(operation) + !visitedOperations.has(operation) ) { - reportedOperations.add(operation); - context.reportDiagnostic({ target: operation }); + // Metadata resolution can report Core diagnostics even for compliant operations. + visitedOperations.add(operation); + if (getLroMetadata(context.program, operation) === undefined) { + context.reportDiagnostic({ target: operation }); + } } } } diff --git a/packages/typespec-azure-resource-manager/test/rules/lro-missing-polling-metadata.test.ts b/packages/typespec-azure-resource-manager/test/rules/lro-missing-polling-metadata.test.ts index 9df40f1f6e..d6780deff2 100644 --- a/packages/typespec-azure-resource-manager/test/rules/lro-missing-polling-metadata.test.ts +++ b/packages/typespec-azure-resource-manager/test/rules/lro-missing-polling-metadata.test.ts @@ -1,6 +1,10 @@ import { Tester } from "#test/tester.js"; import { getSourceLocation } from "@typespec/compiler"; -import { createLinterRuleTester, type LinterRuleTester } from "@typespec/compiler/testing"; +import { + createLinterRuleTester, + type LinterRuleTester, + type TesterInstance, +} from "@typespec/compiler/testing"; import { readFileSync } from "node:fs"; import { beforeEach, expect, it, vi } from "vitest"; import { lroMissingPollingMetadataRule } from "../../src/rules/lro-missing-polling-metadata.js"; @@ -51,11 +55,13 @@ const examples = Array.from( documentation.matchAll(/```tsp\n([\s\S]*?)\n```/g), (match) => match[1], ); +let rawTester: TesterInstance; let tester: LinterRuleTester; beforeEach(async () => { + rawTester = await Tester.createInstance(); tester = createLinterRuleTester( - await Tester.createInstance(), + rawTester, lroMissingPollingMetadataRule, "@azure-tools/typespec-azure-resource-manager", ); @@ -260,3 +266,58 @@ it("ignores unmarked POST accepted responses", async () => { ) .toBeValid(); }); + +it("resolves a compliant operation once before reporting Core diagnostics across overlapping services", async () => { + const diagnostics = await rawTester.diagnose( + ` + @armProviderNamespace + @service + @versioned(Versions) + namespace Microsoft.Parent; + enum Versions { + @useDependency(Azure.ResourceManager.CommonTypes.Versions.v5) + @armCommonTypesVersion(Azure.ResourceManager.CommonTypes.Versions.v3) + v2024_01_01: "2024-01-01", + } + @route("/parent") + interface Operations extends Azure.ResourceManager.Operations {} + + @armProviderNamespace("Microsoft.Nested") + @service + @versioned(Versions) + namespace Nested { + @route("/child") + interface Operations extends Azure.ResourceManager.Operations {} + @armResourceOperations + interface ProviderOperations { + @Azure.Core.useFinalStateVia("original-uri") + startProvider is ArmProviderActionAsync; + } + } + `, + { + compilerOptions: { + linterRuleSet: { + enable: { + "@azure-tools/typespec-azure-resource-manager/lro-missing-polling-metadata": true, + }, + }, + }, + }, + ); + + expect( + diagnostics.filter( + (diagnostic) => + diagnostic.code === "@azure-tools/typespec-azure-core/no-operation-at-original-uri", + ), + ).toHaveLength(1); + expect( + diagnostics.filter( + (diagnostic) => + diagnostic.code === + "@azure-tools/typespec-azure-resource-manager/lro-missing-polling-metadata", + ), + ).toHaveLength(0); + expect(diagnostics).toHaveLength(1); +}); From 285b9891cf9f58cc59cc0f0d92db1d0b72b4f365 Mon Sep 17 00:00:00 2001 From: Yuchao Yan Date: Thu, 15 Oct 2026 10:32:03 +0800 Subject: [PATCH 3/8] test: separate polling rule behavior from integration acceptance Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../lro-polling-metadata-dedup.test.ts | 46 ++++++++ ...lro-polling-metadata-documentation.test.ts | 38 +++++++ ...polling-metadata-package-isolation.test.ts | 31 ++++++ .../lro-missing-polling-metadata.test.ts | 104 +++--------------- 4 files changed, 128 insertions(+), 91 deletions(-) create mode 100644 packages/typespec-azure-resource-manager/test/integration/lro-polling-metadata-dedup.test.ts create mode 100644 packages/typespec-azure-resource-manager/test/integration/lro-polling-metadata-documentation.test.ts create mode 100644 packages/typespec-azure-resource-manager/test/integration/lro-polling-metadata-package-isolation.test.ts diff --git a/packages/typespec-azure-resource-manager/test/integration/lro-polling-metadata-dedup.test.ts b/packages/typespec-azure-resource-manager/test/integration/lro-polling-metadata-dedup.test.ts new file mode 100644 index 0000000000..792c3374fb --- /dev/null +++ b/packages/typespec-azure-resource-manager/test/integration/lro-polling-metadata-dedup.test.ts @@ -0,0 +1,46 @@ +import { expectDiagnostics } from "@typespec/compiler/testing"; +import { it } from "vitest"; +import { Tester } from "../tester.js"; + +it("resolves a compliant operation once before reporting Core diagnostics across overlapping services", async () => { + const diagnostics = await Tester.diagnose( + ` + @armProviderNamespace + @service + @versioned(Versions) + namespace Microsoft.Parent; + enum Versions { + @useDependency(Azure.ResourceManager.CommonTypes.Versions.v5) + @armCommonTypesVersion(Azure.ResourceManager.CommonTypes.Versions.v3) + v2024_01_01: "2024-01-01", + } + @route("/parent") + interface Operations extends Azure.ResourceManager.Operations {} + + @armProviderNamespace("Microsoft.Nested") + @service + @versioned(Versions) + namespace Nested { + @route("/child") + interface Operations extends Azure.ResourceManager.Operations {} + @armResourceOperations + interface ProviderOperations { + @Azure.Core.useFinalStateVia("original-uri") + startProvider is ArmProviderActionAsync; + } + } + `, + { + compilerOptions: { + linterRuleSet: { + enable: { + "@azure-tools/typespec-azure-resource-manager/lro-missing-polling-metadata": true, + }, + }, + }, + }, + ); + expectDiagnostics(diagnostics, { + code: "@azure-tools/typespec-azure-core/no-operation-at-original-uri", + }); +}); diff --git a/packages/typespec-azure-resource-manager/test/integration/lro-polling-metadata-documentation.test.ts b/packages/typespec-azure-resource-manager/test/integration/lro-polling-metadata-documentation.test.ts new file mode 100644 index 0000000000..4248058ff2 --- /dev/null +++ b/packages/typespec-azure-resource-manager/test/integration/lro-polling-metadata-documentation.test.ts @@ -0,0 +1,38 @@ +import { getSourceLocation } from "@typespec/compiler"; +import { createLinterRuleTester } from "@typespec/compiler/testing"; +import { readFileSync } from "node:fs"; +import { expect, it } from "vitest"; +import { lroMissingPollingMetadataRule } from "../../src/rules/lro-missing-polling-metadata.js"; +import { Tester } from "../tester.js"; + +const documentation = readFileSync( + new URL("../../src/rules/lro-missing-polling-metadata.md", import.meta.url), + "utf8", +).replace(/\r\n/g, "\n"); + +it.each([ + ["incorrect", /LroHeaders\s*=\s*PlainLocationHeaders/, true], + ["correct", /LroHeaders\s*=\s*ArmLroLocationHeader { + const example = Array.from( + documentation.matchAll(/```tsp\n([\s\S]*?)\n```/g), + (match) => match[1], + ).find((code) => /update\s+is\s+ArmCustomPatchAsync ({ + code: "@azure-tools/typespec-azure-resource-manager/lro-missing-polling-metadata", + message: lroMissingPollingMetadataRule.messages.default, + pos: getSourceLocation(target).pos, + end: getSourceLocation(target).end, + })); + } else { + await fixture.toBeValid(); + } +}); diff --git a/packages/typespec-azure-resource-manager/test/integration/lro-polling-metadata-package-isolation.test.ts b/packages/typespec-azure-resource-manager/test/integration/lro-polling-metadata-package-isolation.test.ts new file mode 100644 index 0000000000..d9488809d8 --- /dev/null +++ b/packages/typespec-azure-resource-manager/test/integration/lro-polling-metadata-package-isolation.test.ts @@ -0,0 +1,31 @@ +import { createLinterRuleTester } from "@typespec/compiler/testing"; +import { it, vi } from "vitest"; +import { lroMissingPollingMetadataRule } from "../../src/rules/lro-missing-polling-metadata.js"; +import { Tester } from "../tester.js"; + +vi.mock("@azure-tools/typespec-autorest", () => { + throw new Error("Native lint must not load the AutoRest emitter."); +}); +vi.mock("@azure-tools/typespec-client-generator-core", () => { + throw new Error("ARM lint must not load TCGC."); +}); + +it("checks polling metadata without emitter or TCGC packages", async () => { + const tester = createLinterRuleTester( + await Tester.createInstance(), + lroMissingPollingMetadataRule, + "@azure-tools/typespec-azure-resource-manager", + ); + await tester + .expect( + ` + @service namespace SelectedArmProgram; + model Accepted { @statusCode statusCode: 202; @header("Location") location?: string; } + @patch @route("/widgets") op update(): Accepted; + `, + ) + .toEmitDiagnostics({ + code: "@azure-tools/typespec-azure-resource-manager/lro-missing-polling-metadata", + message: lroMissingPollingMetadataRule.messages.default, + }); +}); diff --git a/packages/typespec-azure-resource-manager/test/rules/lro-missing-polling-metadata.test.ts b/packages/typespec-azure-resource-manager/test/rules/lro-missing-polling-metadata.test.ts index d6780deff2..9427fc1238 100644 --- a/packages/typespec-azure-resource-manager/test/rules/lro-missing-polling-metadata.test.ts +++ b/packages/typespec-azure-resource-manager/test/rules/lro-missing-polling-metadata.test.ts @@ -1,21 +1,9 @@ import { Tester } from "#test/tester.js"; import { getSourceLocation } from "@typespec/compiler"; -import { - createLinterRuleTester, - type LinterRuleTester, - type TesterInstance, -} from "@typespec/compiler/testing"; -import { readFileSync } from "node:fs"; -import { beforeEach, expect, it, vi } from "vitest"; +import { createLinterRuleTester, type LinterRuleTester } from "@typespec/compiler/testing"; +import { beforeEach, it } from "vitest"; import { lroMissingPollingMetadataRule } from "../../src/rules/lro-missing-polling-metadata.js"; -vi.mock("@azure-tools/typespec-autorest", () => { - throw new Error("Native lint must not load the AutoRest emitter."); -}); -vi.mock("@azure-tools/typespec-client-generator-core", () => { - throw new Error("ARM lint must not load TCGC."); -}); - const header = ` @armProviderNamespace @service @@ -47,19 +35,10 @@ const diagnostic = { code: "@azure-tools/typespec-azure-resource-manager/lro-missing-polling-metadata", message: lroMissingPollingMetadataRule.messages.default, }; -const documentation = readFileSync( - new URL("../../src/rules/lro-missing-polling-metadata.md", import.meta.url), - "utf8", -).replace(/\r\n/g, "\n"); -const examples = Array.from( - documentation.matchAll(/```tsp\n([\s\S]*?)\n```/g), - (match) => match[1], -); -let rawTester: TesterInstance; let tester: LinterRuleTester; beforeEach(async () => { - rawTester = await Tester.createInstance(); + const rawTester = await Tester.createInstance(); tester = createLinterRuleTester( rawTester, lroMissingPollingMetadataRule, @@ -95,15 +74,11 @@ it("reports a synchronous custom PATCH returning 202 without polling metadata", ); }); -it("reports the documented async PATCH customization without polling metadata", async () => { - expect(examples).toHaveLength(2); - await tester - .expect(examples[0].replace("update is", "/*target*/update is")) - .toEmitDiagnostics(({ target }) => ({ - ...diagnostic, - pos: getSourceLocation(target).pos, - end: getSourceLocation(target).end, - })); +it("reports an async PATCH customization without polling metadata", async () => { + await expectViolation( + "update is ArmCustomPatchAsync;", + "Widgets", + ); }); it("reports a synchronous provider POST returning 202 without polling metadata", async () => { @@ -128,9 +103,11 @@ it("accepts default async custom PATCH polling metadata", async () => { await expectCompliant("update is ArmCustomPatchAsync;", "Widgets"); }); -it("accepts the documented async PATCH with semantic LRO headers", async () => { - expect(examples).toHaveLength(2); - await tester.expect(examples[1]).toBeValid(); +it("accepts an async PATCH with semantic LRO headers", async () => { + await expectCompliant( + "update is ArmCustomPatchAsync & Azure.Core.Foundations.RetryAfterHeader>;", + "Widgets", + ); }); it("accepts synchronous custom PATCH without a 202 response", async () => { @@ -266,58 +243,3 @@ it("ignores unmarked POST accepted responses", async () => { ) .toBeValid(); }); - -it("resolves a compliant operation once before reporting Core diagnostics across overlapping services", async () => { - const diagnostics = await rawTester.diagnose( - ` - @armProviderNamespace - @service - @versioned(Versions) - namespace Microsoft.Parent; - enum Versions { - @useDependency(Azure.ResourceManager.CommonTypes.Versions.v5) - @armCommonTypesVersion(Azure.ResourceManager.CommonTypes.Versions.v3) - v2024_01_01: "2024-01-01", - } - @route("/parent") - interface Operations extends Azure.ResourceManager.Operations {} - - @armProviderNamespace("Microsoft.Nested") - @service - @versioned(Versions) - namespace Nested { - @route("/child") - interface Operations extends Azure.ResourceManager.Operations {} - @armResourceOperations - interface ProviderOperations { - @Azure.Core.useFinalStateVia("original-uri") - startProvider is ArmProviderActionAsync; - } - } - `, - { - compilerOptions: { - linterRuleSet: { - enable: { - "@azure-tools/typespec-azure-resource-manager/lro-missing-polling-metadata": true, - }, - }, - }, - }, - ); - - expect( - diagnostics.filter( - (diagnostic) => - diagnostic.code === "@azure-tools/typespec-azure-core/no-operation-at-original-uri", - ), - ).toHaveLength(1); - expect( - diagnostics.filter( - (diagnostic) => - diagnostic.code === - "@azure-tools/typespec-azure-resource-manager/lro-missing-polling-metadata", - ), - ).toHaveLength(0); - expect(diagnostics).toHaveLength(1); -}); From c2afd8829d55d78013565ae7c7f21ba34fe2c495 Mon Sep 17 00:00:00 2001 From: Yuchao Yan Date: Fri, 16 Oct 2026 10:14:08 +0800 Subject: [PATCH 4/8] test: remove bespoke Markdown documentation parsing Retain literal rule fixtures and package-isolation regressions. Published examples were independently executed and recorded as one-time session evidence; no continuous Markdown coverage is claimed. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- ...intdiff-lro-extension-2026-9-16-10-5-29.md | 7 ++++ ...lro-polling-metadata-documentation.test.ts | 38 ------------------- 2 files changed, 7 insertions(+), 38 deletions(-) create mode 100644 .chronus/changes/promote-lintdiff-lro-extension-2026-9-16-10-5-29.md delete mode 100644 packages/typespec-azure-resource-manager/test/integration/lro-polling-metadata-documentation.test.ts diff --git a/.chronus/changes/promote-lintdiff-lro-extension-2026-9-16-10-5-29.md b/.chronus/changes/promote-lintdiff-lro-extension-2026-9-16-10-5-29.md new file mode 100644 index 0000000000..2e96a6fc8b --- /dev/null +++ b/.chronus/changes/promote-lintdiff-lro-extension-2026-9-16-10-5-29.md @@ -0,0 +1,7 @@ +--- +changeKind: internal +packages: + - "@azure-tools/typespec-azure-resource-manager" +--- + +Remove bespoke Markdown-parsing rule tests; retain literal rule and integration regressions. \ No newline at end of file diff --git a/packages/typespec-azure-resource-manager/test/integration/lro-polling-metadata-documentation.test.ts b/packages/typespec-azure-resource-manager/test/integration/lro-polling-metadata-documentation.test.ts deleted file mode 100644 index 4248058ff2..0000000000 --- a/packages/typespec-azure-resource-manager/test/integration/lro-polling-metadata-documentation.test.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { getSourceLocation } from "@typespec/compiler"; -import { createLinterRuleTester } from "@typespec/compiler/testing"; -import { readFileSync } from "node:fs"; -import { expect, it } from "vitest"; -import { lroMissingPollingMetadataRule } from "../../src/rules/lro-missing-polling-metadata.js"; -import { Tester } from "../tester.js"; - -const documentation = readFileSync( - new URL("../../src/rules/lro-missing-polling-metadata.md", import.meta.url), - "utf8", -).replace(/\r\n/g, "\n"); - -it.each([ - ["incorrect", /LroHeaders\s*=\s*PlainLocationHeaders/, true], - ["correct", /LroHeaders\s*=\s*ArmLroLocationHeader { - const example = Array.from( - documentation.matchAll(/```tsp\n([\s\S]*?)\n```/g), - (match) => match[1], - ).find((code) => /update\s+is\s+ArmCustomPatchAsync ({ - code: "@azure-tools/typespec-azure-resource-manager/lro-missing-polling-metadata", - message: lroMissingPollingMetadataRule.messages.default, - pos: getSourceLocation(target).pos, - end: getSourceLocation(target).end, - })); - } else { - await fixture.toBeValid(); - } -}); From c0f894bbe7fa457175f3ccbda362b5844f830dcd Mon Sep 17 00:00:00 2001 From: Yuchao Yan Date: Mon, 19 Oct 2026 12:16:35 +0800 Subject: [PATCH 5/8] test(arm): consolidate polling resolver regression coverage Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- ...intdiff-lro-extension-2026-9-16-10-5-29.md | 7 --- .../lro-polling-metadata-dedup.test.ts | 46 ---------------- ...polling-metadata-package-isolation.test.ts | 31 ----------- .../test/package-isolation.test.ts | 13 +++++ .../lro-missing-polling-metadata.test.ts | 55 ++++++++++++++++++- 5 files changed, 66 insertions(+), 86 deletions(-) delete mode 100644 .chronus/changes/promote-lintdiff-lro-extension-2026-9-16-10-5-29.md delete mode 100644 packages/typespec-azure-resource-manager/test/integration/lro-polling-metadata-dedup.test.ts delete mode 100644 packages/typespec-azure-resource-manager/test/integration/lro-polling-metadata-package-isolation.test.ts create mode 100644 packages/typespec-azure-resource-manager/test/package-isolation.test.ts diff --git a/.chronus/changes/promote-lintdiff-lro-extension-2026-9-16-10-5-29.md b/.chronus/changes/promote-lintdiff-lro-extension-2026-9-16-10-5-29.md deleted file mode 100644 index 2e96a6fc8b..0000000000 --- a/.chronus/changes/promote-lintdiff-lro-extension-2026-9-16-10-5-29.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -changeKind: internal -packages: - - "@azure-tools/typespec-azure-resource-manager" ---- - -Remove bespoke Markdown-parsing rule tests; retain literal rule and integration regressions. \ No newline at end of file diff --git a/packages/typespec-azure-resource-manager/test/integration/lro-polling-metadata-dedup.test.ts b/packages/typespec-azure-resource-manager/test/integration/lro-polling-metadata-dedup.test.ts deleted file mode 100644 index 792c3374fb..0000000000 --- a/packages/typespec-azure-resource-manager/test/integration/lro-polling-metadata-dedup.test.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { expectDiagnostics } from "@typespec/compiler/testing"; -import { it } from "vitest"; -import { Tester } from "../tester.js"; - -it("resolves a compliant operation once before reporting Core diagnostics across overlapping services", async () => { - const diagnostics = await Tester.diagnose( - ` - @armProviderNamespace - @service - @versioned(Versions) - namespace Microsoft.Parent; - enum Versions { - @useDependency(Azure.ResourceManager.CommonTypes.Versions.v5) - @armCommonTypesVersion(Azure.ResourceManager.CommonTypes.Versions.v3) - v2024_01_01: "2024-01-01", - } - @route("/parent") - interface Operations extends Azure.ResourceManager.Operations {} - - @armProviderNamespace("Microsoft.Nested") - @service - @versioned(Versions) - namespace Nested { - @route("/child") - interface Operations extends Azure.ResourceManager.Operations {} - @armResourceOperations - interface ProviderOperations { - @Azure.Core.useFinalStateVia("original-uri") - startProvider is ArmProviderActionAsync; - } - } - `, - { - compilerOptions: { - linterRuleSet: { - enable: { - "@azure-tools/typespec-azure-resource-manager/lro-missing-polling-metadata": true, - }, - }, - }, - }, - ); - expectDiagnostics(diagnostics, { - code: "@azure-tools/typespec-azure-core/no-operation-at-original-uri", - }); -}); diff --git a/packages/typespec-azure-resource-manager/test/integration/lro-polling-metadata-package-isolation.test.ts b/packages/typespec-azure-resource-manager/test/integration/lro-polling-metadata-package-isolation.test.ts deleted file mode 100644 index d9488809d8..0000000000 --- a/packages/typespec-azure-resource-manager/test/integration/lro-polling-metadata-package-isolation.test.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { createLinterRuleTester } from "@typespec/compiler/testing"; -import { it, vi } from "vitest"; -import { lroMissingPollingMetadataRule } from "../../src/rules/lro-missing-polling-metadata.js"; -import { Tester } from "../tester.js"; - -vi.mock("@azure-tools/typespec-autorest", () => { - throw new Error("Native lint must not load the AutoRest emitter."); -}); -vi.mock("@azure-tools/typespec-client-generator-core", () => { - throw new Error("ARM lint must not load TCGC."); -}); - -it("checks polling metadata without emitter or TCGC packages", async () => { - const tester = createLinterRuleTester( - await Tester.createInstance(), - lroMissingPollingMetadataRule, - "@azure-tools/typespec-azure-resource-manager", - ); - await tester - .expect( - ` - @service namespace SelectedArmProgram; - model Accepted { @statusCode statusCode: 202; @header("Location") location?: string; } - @patch @route("/widgets") op update(): Accepted; - `, - ) - .toEmitDiagnostics({ - code: "@azure-tools/typespec-azure-resource-manager/lro-missing-polling-metadata", - message: lroMissingPollingMetadataRule.messages.default, - }); -}); diff --git a/packages/typespec-azure-resource-manager/test/package-isolation.test.ts b/packages/typespec-azure-resource-manager/test/package-isolation.test.ts new file mode 100644 index 0000000000..12729daf8a --- /dev/null +++ b/packages/typespec-azure-resource-manager/test/package-isolation.test.ts @@ -0,0 +1,13 @@ +import { expect, it, vi } from "vitest"; +import * as library from "../src/index.js"; + +vi.mock("@azure-tools/typespec-autorest", () => { + throw new Error("Native lint must not load the AutoRest emitter."); +}); +vi.mock("@azure-tools/typespec-client-generator-core", () => { + throw new Error("ARM lint must not load TCGC."); +}); + +it("loads the ARM public entry point without emitter or TCGC dependencies", () => { + expect(library.$lib.name).toBe("@azure-tools/typespec-azure-resource-manager"); +}); diff --git a/packages/typespec-azure-resource-manager/test/rules/lro-missing-polling-metadata.test.ts b/packages/typespec-azure-resource-manager/test/rules/lro-missing-polling-metadata.test.ts index 9427fc1238..7113f3e268 100644 --- a/packages/typespec-azure-resource-manager/test/rules/lro-missing-polling-metadata.test.ts +++ b/packages/typespec-azure-resource-manager/test/rules/lro-missing-polling-metadata.test.ts @@ -1,7 +1,11 @@ import { Tester } from "#test/tester.js"; import { getSourceLocation } from "@typespec/compiler"; -import { createLinterRuleTester, type LinterRuleTester } from "@typespec/compiler/testing"; -import { beforeEach, it } from "vitest"; +import { + createLinterRuleTester, + expectDiagnostics, + type LinterRuleTester, +} from "@typespec/compiler/testing"; +import { beforeEach, describe, it } from "vitest"; import { lroMissingPollingMetadataRule } from "../../src/rules/lro-missing-polling-metadata.js"; const header = ` @@ -31,10 +35,12 @@ model PlainLocationHeaders { } interface Operations extends Azure.ResourceManager.Operations {} `; + const diagnostic = { code: "@azure-tools/typespec-azure-resource-manager/lro-missing-polling-metadata", message: lroMissingPollingMetadataRule.messages.default, }; + let tester: LinterRuleTester; beforeEach(async () => { @@ -243,3 +249,48 @@ it("ignores unmarked POST accepted responses", async () => { ) .toBeValid(); }); + +describe("registered resolver side effects", () => { + it("resolves a compliant operation once before reporting Core diagnostics across overlapping services", async () => { + const diagnostics = await Tester.diagnose( + ` + @armProviderNamespace + @service + @versioned(Versions) + namespace Microsoft.Parent; + enum Versions { + @useDependency(Azure.ResourceManager.CommonTypes.Versions.v5) + @armCommonTypesVersion(Azure.ResourceManager.CommonTypes.Versions.v3) + v2024_01_01: "2024-01-01", + } + @route("/parent") + interface Operations extends Azure.ResourceManager.Operations {} + + @armProviderNamespace("Microsoft.Nested") + @service + @versioned(Versions) + namespace Nested { + @route("/child") + interface Operations extends Azure.ResourceManager.Operations {} + @armResourceOperations + interface ProviderOperations { + @Azure.Core.useFinalStateVia("original-uri") + startProvider is ArmProviderActionAsync; + } + } + `, + { + compilerOptions: { + linterRuleSet: { + enable: { + "@azure-tools/typespec-azure-resource-manager/lro-missing-polling-metadata": true, + }, + }, + }, + }, + ); + expectDiagnostics(diagnostics, { + code: "@azure-tools/typespec-azure-core/no-operation-at-original-uri", + }); + }); +}); From 3ca178be4b73a2de7297861fd8d0c33faa78f563 Mon Sep 17 00:00:00 2001 From: Yuchao Yan Date: Tue, 20 Oct 2026 14:18:03 +0800 Subject: [PATCH 6/8] feat(arm): consolidate LRO polling metadata migrations Combine LroExtension and XMSLongRunningOperationProperty in one rule with operation-level diagnostics, shared resolver deduplication, and canonical acceptance coverage. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../lro-missing-polling-metadata-arm.md | 6 +- .../typespec-azure-resource-manager/README.md | 2 +- .../src/rules/lro-missing-polling-metadata.md | 59 +++-- .../src/rules/lro-missing-polling-metadata.ts | 37 ++- .../lro-missing-polling-metadata.test.ts | 216 +++++++++++++++++- .../test/validate-rules-defined.test.ts | 11 + .../content/docs/docs/howtos/ARM/arm-rules.md | 104 ++++----- .../reference/linter.md | 2 +- 8 files changed, 345 insertions(+), 92 deletions(-) diff --git a/.chronus/changes/lro-missing-polling-metadata-arm.md b/.chronus/changes/lro-missing-polling-metadata-arm.md index 208fe168d4..aec2bcc23c 100644 --- a/.chronus/changes/lro-missing-polling-metadata-arm.md +++ b/.chronus/changes/lro-missing-polling-metadata-arm.md @@ -4,6 +4,8 @@ packages: - "@azure-tools/typespec-azure-resource-manager" --- -Add the opt-in `lro-missing-polling-metadata` lint rule for ARM PATCH and provider/collection POST operations returning `202` without native polling metadata. Use asynchronous ARM templates or preserve supported semantic LRO headers. +Add the `lro-missing-polling-metadata` lint rule, consolidating `LroExtension` and `XMSLongRunningOperationProperty`. Require native polling metadata for PATCH and marked provider/collection POST operations returning `202`, and for PUT/PATCH/POST/DELETE operations whose `200`, `201`, `202`, or `204` responses contain `Location` or `Azure-AsyncOperation` headers. -Check each eligible operation once across overlapping service declarations, including compliant operations, to avoid repeated native metadata diagnostics. +Use asynchronous ARM templates or preserve supported semantic LRO headers. Report once on the authored operation even when both checks apply, and resolve each eligible operation once across overlapping service declarations, including compliant operations. + +The rule is disabled in the central resource-manager ruleset and included in the ARM library's compiler-generated `all` ruleset. diff --git a/packages/typespec-azure-resource-manager/README.md b/packages/typespec-azure-resource-manager/README.md index 39a62499a0..973feb2837 100644 --- a/packages/typespec-azure-resource-manager/README.md +++ b/packages/typespec-azure-resource-manager/README.md @@ -66,7 +66,7 @@ Available ruleSets: | [`@azure-tools/typespec-azure-resource-manager/arm-resource-invalid-action-verb`](https://azure.github.io/typespec-azure/docs/libraries/azure-resource-manager/rules/arm-resource-invalid-action-verb) | Actions must be HTTP Post or Get operations. | | [`@azure-tools/typespec-azure-resource-manager/improper-subscription-list-operation`](https://azure.github.io/typespec-azure/docs/libraries/azure-resource-manager/rules/improper-subscription-list-operation) | Tenant and Extension resources should not define a list by subscription operation. | | [`@azure-tools/typespec-azure-resource-manager/lro-location-header`](https://azure.github.io/typespec-azure/docs/libraries/azure-resource-manager/rules/lro-location-header) | A 202 response should include a Location response header. | -| [`@azure-tools/typespec-azure-resource-manager/lro-missing-polling-metadata`](https://azure.github.io/typespec-azure/docs/libraries/azure-resource-manager/rules/lro-missing-polling-metadata) | ARM PATCH and provider/collection POST operations returning 202 must have native polling metadata. | +| [`@azure-tools/typespec-azure-resource-manager/lro-missing-polling-metadata`](https://azure.github.io/typespec-azure/docs/libraries/azure-resource-manager/rules/lro-missing-polling-metadata) | ARM operations indicating asynchronous processing must have native polling metadata. | | [`@azure-tools/typespec-azure-resource-manager/list-operation-missing-pageable`](https://azure.github.io/typespec-azure/docs/libraries/azure-resource-manager/rules/list-operation-missing-pageable) | ARM GET operations on list paths must define TypeSpec paging metadata. | | [`@azure-tools/typespec-azure-resource-manager/missing-x-ms-identifiers`](https://azure.github.io/typespec-azure/docs/libraries/azure-resource-manager/rules/missing-x-ms-identifiers) | Array properties should describe their identifying properties with x-ms-identifiers. Decorate the property with @OpenAPI.extension("x-ms-identifiers", #[id-prop]) where "id-prop" is a list of the names of identifying properties in the item type. | | [`@azure-tools/typespec-azure-resource-manager/no-response-body`](https://azure.github.io/typespec-azure/docs/libraries/azure-resource-manager/rules/no-response-body) | Check that the body is empty for 202 and 204 responses, and not empty for other success (2xx) responses. | diff --git a/packages/typespec-azure-resource-manager/src/rules/lro-missing-polling-metadata.md b/packages/typespec-azure-resource-manager/src/rules/lro-missing-polling-metadata.md index ba6b9956a6..4a7354d5d1 100644 --- a/packages/typespec-azure-resource-manager/src/rules/lro-missing-polling-metadata.md +++ b/packages/typespec-azure-resource-manager/src/rules/lro-missing-polling-metadata.md @@ -1,6 +1,14 @@ -ARM PATCH and provider or collection POST operations returning `202 Accepted` -must describe how clients poll the operation and obtain its final result. -A plain HTTP `Location` header does not supply that polling metadata. +ARM operations indicating asynchronous processing must describe how clients +poll the operation and obtain its final result. This rule combines two checks: + +| Trigger | Scope | +| ------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------- | +| An explicit `202 Accepted` response | All PATCH operations, and POST operations marked as ARM provider or collection actions, even without a polling header | +| A `Location` or `Azure-AsyncOperation` response header | PUT, PATCH, POST, and DELETE operations with an explicit `200`, `201`, `202`, or `204` response | + +Header names are case-insensitive. A plain polling URL or an output-only +`x-ms-long-running-operation: true` extension does not supply native polling +metadata. GET operations, including polling GETs, are outside both checks. Use standard asynchronous ARM templates, and preserve their semantic LRO headers when customizing responses. For example, retain @@ -9,17 +17,25 @@ Synchronous templates can also supply polling metadata through a customized accepted response, but changing only the status code and header name is not sufficient. -This rule checks PATCH and POST operations marked as ARM provider or collection -actions. It reports once on each operation with an explicit `202` response and -no native LRO metadata. Operations without `202` or with native polling metadata -are compliant. Registered resource-instance POST, PUT, and DELETE response -requirements are covered by the existing ARM response-code rules. +The rule checks HTTP operations in service namespaces, without requiring +provider-namespace metadata. Either trigger requires native LRO metadata. +Operations matching both triggers receive only one warning, on the authored +operation rather than a shared response header. Native polling metadata satisfies +both checks. Broad status-code ranges do not substitute for the explicit numeric +statuses above. + +Registered resource-instance POST, PUT, and DELETE operations remain subject to +the polling-header check. Existing ARM response-code and Location-header rules +retain ownership of their separate status-code and header-presence requirements. Operations reached through overlapping service declarations are checked once, including compliant operations. This avoids repeating diagnostics from native polling-metadata resolution. -The rule is available but disabled by default in the resource-manager ruleset. +The combined rule retains one disabled (`false`) entry in the central Azure +resource-manager ruleset. The ARM library's compiler-generated `all` ruleset +includes registered rules, including this one. Consolidation adds no second +public rule or registration; selecting this rule selects both checks. ## Impact @@ -94,12 +110,19 @@ on the affected operation. ## LintDiff Equivalent -This rule originates from -[LroExtension](https://github.com/Azure/azure-openapi-validator/blob/main/docs/lro-extension.md). - -The native implementation follows the reviewed -[lintdiff source rule](https://github.com/Azure/typespec-azure/blob/feature/lintdiff-lro-extension/packages/typespec-lintdiff/src/rules/lro-extension.ts) -at `9afb5818fba089835296258049ad7104e8a92c50`, refreshed from -`f0973f43bffa73169fdde0436566c2534bdab595` to visit eligible operations before -resolving polling metadata. The official ARM rule preserves the destination's -providerless applicability adaptation and remains disabled by default. +| Swagger validator | Combined native coverage | +| ----------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| [LroExtension](https://github.com/Azure/azure-openapi-validator/blob/main/docs/lro-extension.md) | Require polling metadata for PATCH and marked provider/collection POST operations returning `202`; this is partial native coverage of the broader Swagger selector. | +| [XMSLongRunningOperationProperty](https://github.com/Azure/azure-openapi-validator/blob/main/docs/xms-long-running-operation-property.md) | Require polling metadata for PUT/PATCH/POST/DELETE responses with qualifying polling headers and status codes. | + +The reviewed lintdiff sources remain unchanged: +[LroExtension at `9afb5818`](https://github.com/Azure/typespec-azure/blob/9afb5818fba089835296258049ad7104e8a92c50/packages/typespec-lintdiff/src/rules/lro-extension.ts) +and +[XMSLongRunningOperationProperty at `2f7e1d5c`](https://github.com/Azure/typespec-azure/blob/2f7e1d5c3d04230666f7524235bd25eff364eaa8/packages/typespec-lintdiff/src/rules/xms-long-running-operation-property.ts). +Their official destination policies are consolidated here, preserving removal +of the mixed-runner provider-namespace isolation guards. + +The native rule requires supported polling semantics instead of accepting an +emitted extension alone. It checks the supplied program rather than historical +Swagger projections and reports once per authored operation. It does not claim +identical emitted diagnostic counts. diff --git a/packages/typespec-azure-resource-manager/src/rules/lro-missing-polling-metadata.ts b/packages/typespec-azure-resource-manager/src/rules/lro-missing-polling-metadata.ts index 6257692cb4..e3cbf4e7b7 100644 --- a/packages/typespec-azure-resource-manager/src/rules/lro-missing-polling-metadata.ts +++ b/packages/typespec-azure-resource-manager/src/rules/lro-missing-polling-metadata.ts @@ -1,18 +1,22 @@ import { getLroMetadata } from "@azure-tools/typespec-azure-core"; import { createRule, fileRef, listServices, type Operation } from "@typespec/compiler"; -import { getHttpService } from "@typespec/http"; +import { getHttpService, type HttpOperationResponse } from "@typespec/http"; import { isArmCollectionAction } from "../operations.js"; +const pollingStatusCodes = new Set([200, 201, 202, 204]); +const pollingHeaders = new Set(["location", "azure-asyncoperation"]); +const pollingVerbs = new Set(["put", "patch", "post", "delete"]); + export const lroMissingPollingMetadataRule = createRule({ name: "lro-missing-polling-metadata", docs: fileRef.fromPackageRoot("src/rules/lro-missing-polling-metadata.md"), description: - "ARM PATCH and provider/collection POST operations returning 202 must have native polling metadata.", + "ARM operations indicating asynchronous processing must have native polling metadata.", severity: "warning", url: "https://azure.github.io/typespec-azure/docs/libraries/azure-resource-manager/rules/lro-missing-polling-metadata", messages: { default: - "ARM operations returning 202 must have native polling metadata. Use an asynchronous ARM template or preserve supported semantic LRO headers.", + "ARM operations indicating asynchronous processing must have native polling metadata. Use an asynchronous ARM template or preserve supported semantic LRO headers.", }, create(context) { return { @@ -22,14 +26,18 @@ export const lroMissingPollingMetadataRule = createRule({ const [httpService] = getHttpService(context.program, service.type); for (const httpOperation of httpService.operations) { const operation = httpOperation.operation; - const uncovered = + if (visitedOperations.has(operation)) { + continue; + } + const requiresAcceptedMetadata = httpOperation.verb === "patch" || (httpOperation.verb === "post" && isArmCollectionAction(context.program, operation)); - if ( - uncovered && - httpOperation.responses.some((response) => response.statusCodes === 202) && - !visitedOperations.has(operation) - ) { + const hasAcceptedResponse = + requiresAcceptedMetadata && + httpOperation.responses.some((response) => response.statusCodes === 202); + const hasPollingResponse = + pollingVerbs.has(httpOperation.verb) && hasPollingHeader(httpOperation.responses); + if (hasAcceptedResponse || hasPollingResponse) { // Metadata resolution can report Core diagnostics even for compliant operations. visitedOperations.add(operation); if (getLroMetadata(context.program, operation) === undefined) { @@ -42,3 +50,14 @@ export const lroMissingPollingMetadataRule = createRule({ }; }, }); + +function hasPollingHeader(responses: HttpOperationResponse[]): boolean { + return responses.some( + (response) => + typeof response.statusCodes === "number" && + pollingStatusCodes.has(response.statusCodes) && + response.responses.some((content) => + Object.keys(content.headers ?? {}).some((name) => pollingHeaders.has(name.toLowerCase())), + ), + ); +} diff --git a/packages/typespec-azure-resource-manager/test/rules/lro-missing-polling-metadata.test.ts b/packages/typespec-azure-resource-manager/test/rules/lro-missing-polling-metadata.test.ts index 7113f3e268..60f53bbeb6 100644 --- a/packages/typespec-azure-resource-manager/test/rules/lro-missing-polling-metadata.test.ts +++ b/packages/typespec-azure-resource-manager/test/rules/lro-missing-polling-metadata.test.ts @@ -3,9 +3,11 @@ import { getSourceLocation } from "@typespec/compiler"; import { createLinterRuleTester, expectDiagnostics, + t, type LinterRuleTester, } from "@typespec/compiler/testing"; -import { beforeEach, describe, it } from "vitest"; +import { getHttpOperation } from "@typespec/http"; +import { beforeEach, describe, expect, it } from "vitest"; import { lroMissingPollingMetadataRule } from "../../src/rules/lro-missing-polling-metadata.js"; const header = ` @@ -101,8 +103,36 @@ it("reports an async provider POST with plain Location headers", async () => { ); }); -it("accepts standard async resource PATCH polling metadata", async () => { - await expectCompliant("update is ArmResourcePatchAsync;", "Widgets"); +it("accepts standard async resource operations with qualifying polling responses", async () => { + await tester + .expect( + t.code`${header} + @armResourceOperations + interface Widgets { + ${t.op("createOrUpdate")} is ArmResourceCreateOrReplaceAsync; + ${t.op("update")} is ArmResourcePatchAsync; + ${t.op("remove")} is ArmResourceDeleteWithoutOkAsync; + } + `, + ) + .toEmitDiagnostics(({ program, createOrUpdate, update, remove }) => { + for (const operation of [createOrUpdate, update, remove]) { + const [httpOperation] = getHttpOperation(program, operation); + expect( + httpOperation.responses.some( + (response) => + typeof response.statusCodes === "number" && + [200, 201, 202, 204].includes(response.statusCodes) && + response.responses.some((content) => + Object.keys(content.headers ?? {}).some((name) => + ["location", "azure-asyncoperation"].includes(name.toLowerCase()), + ), + ), + ), + ).toBe(true); + } + return []; + }); }); it("accepts default async custom PATCH polling metadata", async () => { @@ -217,19 +247,19 @@ it("checks PATCH without provider metadata when the ARM rule is selected", async })); }); -it("leaves registered resource-instance POST response-code ownership to the existing rule", async () => { - await expectCompliant( +it("reports polling headers on registered resource-instance POST operations", async () => { + await expectViolation( "startWidget is ArmResourceActionSync>;", "Widgets", ); }); -it("ignores GET PUT and DELETE accepted responses", async () => { +it("ignores GET PUT and DELETE accepted responses without polling headers", async () => { await tester .expect( ` @service namespace SelectedArmProgram; - model Accepted { @statusCode statusCode: 202; @header("Location") location?: string; } + model Accepted { @statusCode statusCode: 202; } @get @route("/get") op read(): Accepted; @put @route("/put") op create(): Accepted; @delete @route("/delete") op remove(): Accepted; @@ -238,18 +268,186 @@ it("ignores GET PUT and DELETE accepted responses", async () => { .toBeValid(); }); -it("ignores unmarked POST accepted responses", async () => { +it("ignores unmarked POST accepted responses without polling headers", async () => { await tester .expect( ` @service namespace SelectedArmProgram; - model Accepted { @statusCode statusCode: 202; @header("Location") location?: string; } + model Accepted { @statusCode statusCode: 202; } @post @route("/action") op act(): Accepted; `, ) .toBeValid(); }); +it("reports PATCH 202 even without a polling header", async () => { + await tester + .expect( + ` + @service namespace SelectedArmProgram; + @patch op /*target*/update(): { @statusCode statusCode: 202; }; + `, + ) + .toEmitDiagnostics(({ target }) => ({ + ...diagnostic, + pos: getSourceLocation(target).pos, + end: getSourceLocation(target).end, + })); +}); + +it("reports a marked provider POST 202 even without a polling header", async () => { + await tester + .expect( + `${header} + model Accepted { @statusCode statusCode: 202; } + @armResourceOperations + interface ProviderOperations { + /*target*/start is ArmProviderActionSync; + } + `, + ) + .toEmitDiagnostics(({ target }) => ({ + ...diagnostic, + pos: getSourceLocation(target).pos, + end: getSourceLocation(target).end, + })); +}); + +it.each([ + ["put", 201, "Azure-AsyncOperation"], + ["patch", 200, "lOcAtIoN"], + ["post", 202, "Location"], + ["delete", 204, "azure-asyncoperation"], +])("reports %s %s with %s on the operation", async (verb, status, name) => { + await tester + .expect( + ` + @service namespace SelectedArmProgram; + model Response { + @statusCode statusCode: ${status}; + @header("${name}") pollingUrl?: string; + } + @${verb} op /*target*/test(): Response; + `, + ) + .toEmitDiagnostics(({ target }) => ({ + ...diagnostic, + pos: getSourceLocation(target).pos, + end: getSourceLocation(target).end, + })); +}); + +it.each([ + ["get", 202, "Location"], + ["head", 200, "Azure-AsyncOperation"], + ["post", 203, "Location"], + ["post", 400, "Azure-AsyncOperation"], + ["post", 200, "Operation-Location"], + ["post", 200, "Retry-After"], +])("ignores %s %s with %s", async (verb, status, name) => { + await tester + .expect( + ` + @service namespace SelectedArmProgram; + @${verb} op test(): { + @statusCode statusCode: ${status}; + @header("${name}") pollingUrl?: string; + }; + `, + ) + .toBeValid(); +}); + +it("reports a qualifying header after an unrelated header", async () => { + await tester + .expect( + ` + @service namespace SelectedArmProgram; + @post op /*target*/test(): { + @statusCode statusCode: 200; + @header("Retry-After") retryAfter?: int32; + @header("Location") pollingUrl?: string; + }; + `, + ) + .toEmitDiagnostics(({ target }) => ({ + ...diagnostic, + pos: getSourceLocation(target).pos, + end: getSourceLocation(target).end, + })); +}); + +it("does not accept an output-only LRO extension as polling metadata", async () => { + const runner = await Tester.import("@typespec/openapi").createInstance(); + const extensionTester = createLinterRuleTester( + runner, + lroMissingPollingMetadataRule, + "@azure-tools/typespec-azure-resource-manager", + ); + await extensionTester + .expect( + ` + @service namespace SelectedArmProgram; + @OpenAPI.extension("x-ms-long-running-operation", true) + @post op /*target*/test(): { + @statusCode statusCode: 200; + @header("Location") pollingUrl?: string; + }; + `, + ) + .toEmitDiagnostics(({ target }) => ({ + ...diagnostic, + pos: getSourceLocation(target).pos, + end: getSourceLocation(target).end, + })); +}); + +it("reports once when several responses and headers require polling metadata", async () => { + await tester + .expect( + ` + @service namespace SelectedArmProgram; + model Created { + @statusCode statusCode: 201; + @header("Azure-AsyncOperation") pollingUrl?: string; + } + model Accepted { + @statusCode statusCode: 202; + @header("Location") location?: string; + @header("Azure-AsyncOperation") pollingUrl?: string; + } + @patch op /*target*/update(): Created | Accepted; + `, + ) + .toEmitDiagnostics(({ target }) => ({ + ...diagnostic, + pos: getSourceLocation(target).pos, + end: getSourceLocation(target).end, + })); +}); + +it("reports each header-triggered operation when response properties are shared", async () => { + await tester + .expect( + ` + @service namespace SelectedArmProgram; + model Response { + @statusCode statusCode: 201; + @header("Location") pollingUrl?: string; + } + @put @route("/one") op /*first*/first(): Response; + @put @route("/two") op /*second*/second(): Response; + `, + ) + .toEmitDiagnostics(({ first, second }) => + [first, second].map((operation) => ({ + ...diagnostic, + pos: getSourceLocation(operation).pos, + end: getSourceLocation(operation).end, + })), + ); +}); + describe("registered resolver side effects", () => { it("resolves a compliant operation once before reporting Core diagnostics across overlapping services", async () => { const diagnostics = await Tester.diagnose( diff --git a/packages/typespec-azure-rulesets/test/validate-rules-defined.test.ts b/packages/typespec-azure-rulesets/test/validate-rules-defined.test.ts index 35983c044b..4a1c53f440 100644 --- a/packages/typespec-azure-rulesets/test/validate-rules-defined.test.ts +++ b/packages/typespec-azure-rulesets/test/validate-rules-defined.test.ts @@ -79,4 +79,15 @@ describe("expect all rules to be defined", () => { strictEqual($linter.ruleSets?.[rulesetName].enable?.[ruleName], false); } }); + + it("keeps the combined ARM polling metadata rule disabled in the central ruleset", () => { + const name = "lro-missing-polling-metadata"; + strictEqual(ResourceManagerLinter.rules.filter((rule) => rule.name === name).length, 1); + strictEqual( + $linter.ruleSets?.["resource-manager"].enable?.[ + `@azure-tools/typespec-azure-resource-manager/${name}` + ], + false, + ); + }); }); diff --git a/website/src/content/docs/docs/howtos/ARM/arm-rules.md b/website/src/content/docs/docs/howtos/ARM/arm-rules.md index ce5bd61602..3afd1c1e47 100644 --- a/website/src/content/docs/docs/howtos/ARM/arm-rules.md +++ b/website/src/content/docs/docs/howtos/ARM/arm-rules.md @@ -97,58 +97,58 @@ The tables below provide guidance to rule authors and ARM reviewers on how to ev ### Rules in @azure-tools/typespec-azure-resource-manager -| Rule | LintDiff | Impact | -| ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| [`arm-common-types-version`](https://azure.github.io/typespec-azure/docs/libraries/azure-resource-manager/rules/arm-common-types-version/) | — | **API, SDK.** Indicates that common-types are not used, though this should produce other violations. | -| [`arm-custom-resource-no-key`](https://azure.github.io/typespec-azure/docs/libraries/azure-resource-manager/rules/arm-custom-resource-no-key/) | — | **API, SDK.** Mainly a correctness issue; the name property should use `@key`. | -| [`arm-custom-resource-usage-discourage`](https://azure.github.io/typespec-azure/docs/libraries/azure-resource-manager/rules/arm-custom-resource-usage-discourage/) | — | **API.** The resource does not use ARM common-types. | -| [`arm-delete-operation-response-codes`](https://azure.github.io/typespec-azure/docs/libraries/azure-resource-manager/rules/arm-delete-operation-response-codes/) | [`DeleteResponseCodes`](https://github.com/Azure/azure-openapi-validator/blob/main/docs/delete-response-codes.md) | **API.** RPC violation. | -| [`arm-no-path-casing-conflicts`](https://azure.github.io/typespec-azure/docs/libraries/azure-resource-manager/rules/arm-no-path-casing-conflicts/) | — | **API, SDK.** Different-case paths to the same resource can cause ARM manifest failures. | -| [`arm-no-record`](https://azure.github.io/typespec-azure/docs/libraries/azure-resource-manager/rules/arm-no-record/) | [`AvoidAdditionalProperties`](https://github.com/Azure/azure-openapi-validator/blob/main/docs/avoid-additional-properties.md) | **API, SDK.** Dictionary types make the API and SDK difficult to use. | -| [`arm-post-operation-response-codes`](https://azure.github.io/typespec-azure/docs/libraries/azure-resource-manager/rules/arm-post-operation-response-codes/) | [`PostResponseCodes`](https://github.com/Azure/azure-openapi-validator/blob/main/docs/post-response-codes.md) | **API.** RPC violation. | -| [`arm-put-operation-response-codes`](https://azure.github.io/typespec-azure/docs/libraries/azure-resource-manager/rules/arm-put-operation-response-codes/) | [`PutResponseCodes`](https://github.com/Azure/azure-openapi-validator/blob/main/docs/put-response-codes.md) | **API.** RPC violation. | -| [`arm-resource-action-no-segment`](https://azure.github.io/typespec-azure/docs/libraries/azure-resource-manager/rules/arm-resource-action-no-segment/) | [`PathForResourceAction`](https://github.com/Azure/azure-openapi-validator/blob/main/docs/path-for-resource-action.md) (partial) | **API, SDK.** May indicate a violation of the resource action path. | -| [`arm-resource-duplicate-property`](https://azure.github.io/typespec-azure/docs/libraries/azure-resource-manager/rules/arm-resource-duplicate-property/) | [R3019](https://github.com/Azure/azure-rest-api-specs/blob/main/documentation/openapi-authoring-automated-guidelines.md#r3019) | **API.** RPC violation. | -| [`arm-resource-interface-requires-decorator`](https://azure.github.io/typespec-azure/docs/libraries/azure-resource-manager/rules/arm-resource-interface-requires-decorator/) | — | No impact beyond what other rules cover. | -| [`arm-resource-invalid-action-verb`](https://azure.github.io/typespec-azure/docs/libraries/azure-resource-manager/rules/arm-resource-invalid-action-verb/) | — | **API, SDK.** An improper HTTP verb for a resource action can cause failures in the C# SDK and in emitters that model resource actions, such as service generation and the portal. | -| [`arm-resource-invalid-envelope-property`](https://azure.github.io/typespec-azure/docs/libraries/azure-resource-manager/rules/arm-resource-invalid-envelope-property/) | [R3006](https://github.com/Azure/azure-rest-api-specs/blob/main/documentation/openapi-authoring-automated-guidelines.md#r3006) | **API.** RPC violation. | -| [`arm-resource-invalid-version-format`](https://azure.github.io/typespec-azure/docs/libraries/azure-resource-manager/rules/arm-resource-invalid-version-format/) | [R3012](https://github.com/Azure/azure-rest-api-specs/blob/main/documentation/openapi-authoring-automated-guidelines.md#r3012) | **API.** RPC violation. | -| [`arm-resource-key-invalid-chars`](https://azure.github.io/typespec-azure/docs/libraries/azure-resource-manager/rules/arm-resource-key-invalid-chars/) | — | **SDK.** The resource parameter name produces invalid keys in SDKs. | -| [`arm-resource-name-pattern`](https://azure.github.io/typespec-azure/docs/libraries/azure-resource-manager/rules/arm-resource-name-pattern/) | [`ResourceNameRestriction`](https://github.com/Azure/azure-openapi-validator/blob/main/docs/resource-name-restriction.md) | **API.** RPC violation. | -| [`arm-resource-operation-response`](https://azure.github.io/typespec-azure/docs/libraries/azure-resource-manager/rules/arm-resource-operation-response/) | [R3007](https://github.com/Azure/azure-rest-api-specs/blob/main/documentation/openapi-authoring-automated-guidelines.md#r3007) (the TypeSpec rule also covers `list`) | **API.** RPC violation. | -| [`arm-resource-patch`](https://azure.github.io/typespec-azure/docs/libraries/azure-resource-manager/rules/arm-resource-patch/) | [`ConsistentPatchProperties`](https://github.com/Azure/azure-openapi-validator/blob/main/docs/consistent-patch-properties.md) | **API.** RPC violation. | -| [`arm-resource-path-segment-invalid-chars`](https://azure.github.io/typespec-azure/docs/libraries/azure-resource-manager/rules/arm-resource-path-segment-invalid-chars/) | — | **SDK, API.** Produces an invalid ARM API and invalid parameter names. | -| [`arm-resource-provisioning-state`](https://azure.github.io/typespec-azure/docs/libraries/azure-resource-manager/rules/arm-resource-provisioning-state/) | [`ProvisioningStateValidation`](https://github.com/Azure/azure-openapi-validator/blob/main/docs/provisioning-state-validation.md), [`ProvisioningStateSpecifiedForLROPut`](https://github.com/Azure/azure-openapi-validator/blob/main/docs/lro-put-provisioning-state-specified.md), [`ProvisioningStateSpecifiedForLROPatch`](https://github.com/Azure/azure-openapi-validator/blob/main/docs/lro-patch-provisioning-state-specified.md), [`RpaaS_ResourceProvisioningState`](https://github.com/Azure/azure-openapi-validator/blob/main/docs/rpaas-resource-provisioning-state.md) | **API.** RPC / RPaaS violation. | -| [`beyond-nesting-levels`](https://azure.github.io/typespec-azure/docs/libraries/azure-resource-manager/rules/beyond-nesting-levels/) | [`TrackedResourceBeyondsThirdLevel`](https://github.com/Azure/azure-openapi-validator/blob/main/docs/tracked-resource-beyond-thrid-level.md) | **API.** RPC violation. | -| [`empty-updateable-properties`](https://azure.github.io/typespec-azure/docs/libraries/azure-resource-manager/rules/empty-updateable-properties/) | — | **API.** Covered by patch-specific rules. | -| [`improper-subscription-list-operation`](https://azure.github.io/typespec-azure/docs/libraries/azure-resource-manager/rules/improper-subscription-list-operation/) | — | **API.** Likely a modeling error; only subscription-based resources should have list operations. | -| [`list-operation-missing-pageable`](https://azure.github.io/typespec-azure/docs/libraries/azure-resource-manager/rules/list-operation-missing-pageable/) | [`XmsPageableForListCalls`](https://github.com/Azure/azure-openapi-validator/blob/main/docs/xms-pageable-for-list-calls.md) | **API, SDK.** Missing paging metadata makes generated list methods inconsistent and makes adding continuation links a breaking change. | -| [`list-response-envelope`](https://azure.github.io/typespec-azure/docs/libraries/azure-resource-manager/rules/list-response-envelope/) | [`CollectionObjectPropertiesNaming`](https://github.com/Azure/azure-openapi-validator/blob/main/docs/collection-object-properties-naming.md), [`GetCollectionOnlyHasValueAndNextLink`](https://github.com/Azure/azure-openapi-validator/blob/main/packages/rulesets/src/spectral/functions/get-collection-only-has-value-and-next-link.ts) | **API, SDK.** A nonstandard collection envelope or missing/non-array `value` property disrupts paged collection responses and client pagination. | -| [`lro-location-header`](https://azure.github.io/typespec-azure/docs/libraries/azure-resource-manager/rules/lro-location-header/) | [`LroLocationHeader`](https://github.com/Azure/azure-openapi-validator/blob/main/docs/lro-location-header.md) | **API.** RPC violation. | -| [`lro-missing-polling-metadata`](https://azure.github.io/typespec-azure/docs/libraries/azure-resource-manager/rules/lro-missing-polling-metadata/) | [`LroExtension`](https://github.com/Azure/azure-openapi-validator/blob/main/docs/lro-extension.md) (partial) | **API, SDK.** ARM PATCH and provider/collection POST responses with `202` need native polling metadata to describe polling and final results. Disabled by default. | -| [`lro-response-mismatch`](https://azure.github.io/typespec-azure/docs/libraries/azure-resource-manager/rules/lro-response-mismatch/) | — | **API, SDK.** A final result that differs from the operation response can break generated long-running operation methods. | -| [`missing-operations-endpoint`](https://azure.github.io/typespec-azure/docs/libraries/azure-resource-manager/rules/missing-operations-endpoint/) | [R3023](https://github.com/Azure/azure-rest-api-specs/blob/main/documentation/openapi-authoring-automated-guidelines.md#r3023) | **API.** RPC violation. | -| [`missing-x-ms-identifiers`](https://azure.github.io/typespec-azure/docs/libraries/azure-resource-manager/rules/missing-x-ms-identifiers/) | [R4041](https://github.com/Azure/azure-rest-api-specs/blob/main/documentation/openapi-authoring-automated-guidelines.md#r4041) (warning) | **API.** No real impact (an old pattern). | -| [`no-empty-model`](https://azure.github.io/typespec-azure/docs/libraries/azure-resource-manager/rules/no-empty-model/) | [R4037](https://github.com/Azure/azure-rest-api-specs/blob/main/documentation/openapi-authoring-automated-guidelines.md#r4037) | **API, SDK.** Accepting any schema makes the API and SDK difficult to use. | -| [`no-override-props`](https://azure.github.io/typespec-azure/docs/libraries/azure-resource-manager/rules/no-override-props/) | — | **SDK, Tooling.** Violations can crash the breaking-change tool and are unsupported by most languages. | -| [`no-query-in-collection`](https://azure.github.io/typespec-azure/docs/libraries/azure-resource-manager/rules/no-query-in-collection/) | [`QueryParametersInCollectionGet`](https://github.com/Azure/azure-openapi-validator/blob/main/packages/rulesets/src/spectral/functions/query-parameters-in-collection-get.ts) | **API, SDK.** Nonstandard collection query parameters make list operations and generated SDK signatures inconsistent. | -| [`no-query-in-point-op`](https://azure.github.io/typespec-azure/docs/libraries/azure-resource-manager/rules/no-query-in-point-op/) | [`ValidQueryParametersForPointOperations`](https://github.com/Azure/azure-openapi-validator/blob/main/docs/valid-query-parameters-for-point-operations.md) | **API, SDK.** Point-operation query parameters beyond `api-version` violate the ARM request contract and complicate SDK signatures. | -| [`no-query-in-post`](https://azure.github.io/typespec-azure/docs/libraries/azure-resource-manager/rules/no-query-in-post/) | [`ParametersInPost`](https://github.com/Azure/azure-openapi-validator/blob/main/docs/parameters-in-post.md) | **API, SDK.** POST inputs outside the request body create nonstandard APIs and unstable SDK signatures. | -| [`no-resource-delete-operation`](https://azure.github.io/typespec-azure/docs/libraries/azure-resource-manager/rules/no-resource-delete-operation/) | [`AllTrackedResourcesMustHaveDelete`](https://github.com/Azure/azure-openapi-validator/blob/main/docs/all-tracked-resources-must-have-delete.md) | **API.** RPC violation. | -| [`no-response-body`](https://azure.github.io/typespec-azure/docs/libraries/azure-resource-manager/rules/no-response-body/) | — | **API.** A non-empty response, usually for a 202. | -| [`no-tenant-level-apis`](https://azure.github.io/typespec-azure/docs/libraries/azure-resource-manager/rules/no-tenant-level-apis/) | [`TenantLevelAPIsNotAllowed`](https://github.com/Azure/azure-openapi-validator/blob/main/docs/tenant-level-apis-not-allowed.md) | **API, Security.** Tenant-level PUT APIs bypass subscription and resource-group boundaries and require additional security review. | -| [`patch-envelope`](https://azure.github.io/typespec-azure/docs/libraries/azure-resource-manager/rules/patch-envelope/) | — | **API.** A Patch operation is missing updatable envelope properties. | -| [`resource-name`](https://azure.github.io/typespec-azure/docs/libraries/azure-resource-manager/rules/resource-name/) | — | **API, SDK.** Invalid characters in a name violate the RPC and create invalid client parameter names, which prevents SDK generation. | -| [`scope-path-segment-casing`](https://azure.github.io/typespec-azure/docs/libraries/azure-resource-manager/rules/scope-path-segment-casing/) | [`SubscriptionsAndResourceGroupCasing`](https://github.com/Azure/azure-openapi-validator/blob/main/docs/subscriptions-and-resource-group-casing.md) | **API, SDK.** Incorrect subscription and resource-group scope casing can prevent resource-provider registration and produce inconsistent client URLs. | -| [`secret-prop`](https://azure.github.io/typespec-azure/docs/libraries/azure-resource-manager/rules/secret-prop/) | [`XMSSecretInResponse`](https://github.com/Azure/azure-openapi-validator/blob/main/docs/xms-secret-in-response.md) | **API.** RPC violation. | -| [`unsupported-type`](https://azure.github.io/typespec-azure/docs/libraries/azure-resource-manager/rules/unsupported-type/) | — | **SDK.** The data type cannot be modeled in SDKs. | -| [`use-api-version`](https://azure.github.io/typespec-azure/docs/libraries/azure-resource-manager/rules/use-api-version/) | [`ApiVersionParameterRequired`](https://github.com/Azure/azure-openapi-validator/blob/main/docs/api-version-parameter-required.md) | **API, SDK.** An operation without the standard API version parameter cannot evolve safely and is difficult for SDKs to represent consistently. | -| [`use-application-json-content-type`](https://azure.github.io/typespec-azure/docs/libraries/azure-resource-manager/rules/use-application-json-content-type/) | [`NonApplicationJsonType`](https://github.com/Azure/azure-openapi-validator/blob/main/packages/rulesets/src/spectral/az-arm.ts) | **API, SDK.** Non-JSON request or response bodies are incompatible with the ARM contract and produce inconsistent serialization. | -| [`use-create-for-put`](https://azure.github.io/typespec-azure/docs/libraries/typespec-client-generator-core/rules/use-create-for-put/) | [PutInOperationName (R1006)](https://github.com/Azure/azure-rest-api-specs/blob/main/documentation/openapi-authoring-automated-guidelines.md#r1006) | **SDK.** TCGC checks the common SDK method name, including unscoped `@clientName` overrides; renaming a shipped method may break SDK compatibility. | -| [`use-interface`](https://azure.github.io/typespec-azure/docs/libraries/azure-resource-manager/rules/use-interface/) | — | **API, SDK, Emitters.** Resource operations outside interfaces can break ARM resource-operation modeling and downstream tooling assumptions. | -| [`use-model-request-body`](https://azure.github.io/typespec-azure/docs/libraries/azure-resource-manager/rules/use-model-request-body/) | [`ParametersSchemaAsTypeObject`](https://github.com/Azure/azure-openapi-validator/blob/main/docs/parameters-schema-as-type-object.md) | **API, SDK.** Non-model, array, and record request bodies cannot evolve with additional properties without a breaking change. | -| [`use-operation-decorator`](https://azure.github.io/typespec-azure/docs/libraries/azure-resource-manager/rules/use-operation-decorator/) | — | **API, SDK, Emitters.** Missing or mismatched decorators prevent resource operations from being associated with the correct resource, which can break SDK generation and resource-aware tooling. | -| [`version-progression`](https://azure.github.io/typespec-azure/docs/libraries/azure-resource-manager/rules/version-progression/) | — | **API, SDK, Tooling.** Out-of-order versions make specs unmaintainable, and versions with matching dates cause SDK problems. | +| Rule | LintDiff | Impact | +| ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| [`arm-common-types-version`](https://azure.github.io/typespec-azure/docs/libraries/azure-resource-manager/rules/arm-common-types-version/) | — | **API, SDK.** Indicates that common-types are not used, though this should produce other violations. | +| [`arm-custom-resource-no-key`](https://azure.github.io/typespec-azure/docs/libraries/azure-resource-manager/rules/arm-custom-resource-no-key/) | — | **API, SDK.** Mainly a correctness issue; the name property should use `@key`. | +| [`arm-custom-resource-usage-discourage`](https://azure.github.io/typespec-azure/docs/libraries/azure-resource-manager/rules/arm-custom-resource-usage-discourage/) | — | **API.** The resource does not use ARM common-types. | +| [`arm-delete-operation-response-codes`](https://azure.github.io/typespec-azure/docs/libraries/azure-resource-manager/rules/arm-delete-operation-response-codes/) | [`DeleteResponseCodes`](https://github.com/Azure/azure-openapi-validator/blob/main/docs/delete-response-codes.md) | **API.** RPC violation. | +| [`arm-no-path-casing-conflicts`](https://azure.github.io/typespec-azure/docs/libraries/azure-resource-manager/rules/arm-no-path-casing-conflicts/) | — | **API, SDK.** Different-case paths to the same resource can cause ARM manifest failures. | +| [`arm-no-record`](https://azure.github.io/typespec-azure/docs/libraries/azure-resource-manager/rules/arm-no-record/) | [`AvoidAdditionalProperties`](https://github.com/Azure/azure-openapi-validator/blob/main/docs/avoid-additional-properties.md) | **API, SDK.** Dictionary types make the API and SDK difficult to use. | +| [`arm-post-operation-response-codes`](https://azure.github.io/typespec-azure/docs/libraries/azure-resource-manager/rules/arm-post-operation-response-codes/) | [`PostResponseCodes`](https://github.com/Azure/azure-openapi-validator/blob/main/docs/post-response-codes.md) | **API.** RPC violation. | +| [`arm-put-operation-response-codes`](https://azure.github.io/typespec-azure/docs/libraries/azure-resource-manager/rules/arm-put-operation-response-codes/) | [`PutResponseCodes`](https://github.com/Azure/azure-openapi-validator/blob/main/docs/put-response-codes.md) | **API.** RPC violation. | +| [`arm-resource-action-no-segment`](https://azure.github.io/typespec-azure/docs/libraries/azure-resource-manager/rules/arm-resource-action-no-segment/) | [`PathForResourceAction`](https://github.com/Azure/azure-openapi-validator/blob/main/docs/path-for-resource-action.md) (partial) | **API, SDK.** May indicate a violation of the resource action path. | +| [`arm-resource-duplicate-property`](https://azure.github.io/typespec-azure/docs/libraries/azure-resource-manager/rules/arm-resource-duplicate-property/) | [R3019](https://github.com/Azure/azure-rest-api-specs/blob/main/documentation/openapi-authoring-automated-guidelines.md#r3019) | **API.** RPC violation. | +| [`arm-resource-interface-requires-decorator`](https://azure.github.io/typespec-azure/docs/libraries/azure-resource-manager/rules/arm-resource-interface-requires-decorator/) | — | No impact beyond what other rules cover. | +| [`arm-resource-invalid-action-verb`](https://azure.github.io/typespec-azure/docs/libraries/azure-resource-manager/rules/arm-resource-invalid-action-verb/) | — | **API, SDK.** An improper HTTP verb for a resource action can cause failures in the C# SDK and in emitters that model resource actions, such as service generation and the portal. | +| [`arm-resource-invalid-envelope-property`](https://azure.github.io/typespec-azure/docs/libraries/azure-resource-manager/rules/arm-resource-invalid-envelope-property/) | [R3006](https://github.com/Azure/azure-rest-api-specs/blob/main/documentation/openapi-authoring-automated-guidelines.md#r3006) | **API.** RPC violation. | +| [`arm-resource-invalid-version-format`](https://azure.github.io/typespec-azure/docs/libraries/azure-resource-manager/rules/arm-resource-invalid-version-format/) | [R3012](https://github.com/Azure/azure-rest-api-specs/blob/main/documentation/openapi-authoring-automated-guidelines.md#r3012) | **API.** RPC violation. | +| [`arm-resource-key-invalid-chars`](https://azure.github.io/typespec-azure/docs/libraries/azure-resource-manager/rules/arm-resource-key-invalid-chars/) | — | **SDK.** The resource parameter name produces invalid keys in SDKs. | +| [`arm-resource-name-pattern`](https://azure.github.io/typespec-azure/docs/libraries/azure-resource-manager/rules/arm-resource-name-pattern/) | [`ResourceNameRestriction`](https://github.com/Azure/azure-openapi-validator/blob/main/docs/resource-name-restriction.md) | **API.** RPC violation. | +| [`arm-resource-operation-response`](https://azure.github.io/typespec-azure/docs/libraries/azure-resource-manager/rules/arm-resource-operation-response/) | [R3007](https://github.com/Azure/azure-rest-api-specs/blob/main/documentation/openapi-authoring-automated-guidelines.md#r3007) (the TypeSpec rule also covers `list`) | **API.** RPC violation. | +| [`arm-resource-patch`](https://azure.github.io/typespec-azure/docs/libraries/azure-resource-manager/rules/arm-resource-patch/) | [`ConsistentPatchProperties`](https://github.com/Azure/azure-openapi-validator/blob/main/docs/consistent-patch-properties.md) | **API.** RPC violation. | +| [`arm-resource-path-segment-invalid-chars`](https://azure.github.io/typespec-azure/docs/libraries/azure-resource-manager/rules/arm-resource-path-segment-invalid-chars/) | — | **SDK, API.** Produces an invalid ARM API and invalid parameter names. | +| [`arm-resource-provisioning-state`](https://azure.github.io/typespec-azure/docs/libraries/azure-resource-manager/rules/arm-resource-provisioning-state/) | [`ProvisioningStateValidation`](https://github.com/Azure/azure-openapi-validator/blob/main/docs/provisioning-state-validation.md), [`ProvisioningStateSpecifiedForLROPut`](https://github.com/Azure/azure-openapi-validator/blob/main/docs/lro-put-provisioning-state-specified.md), [`ProvisioningStateSpecifiedForLROPatch`](https://github.com/Azure/azure-openapi-validator/blob/main/docs/lro-patch-provisioning-state-specified.md), [`RpaaS_ResourceProvisioningState`](https://github.com/Azure/azure-openapi-validator/blob/main/docs/rpaas-resource-provisioning-state.md) | **API.** RPC / RPaaS violation. | +| [`beyond-nesting-levels`](https://azure.github.io/typespec-azure/docs/libraries/azure-resource-manager/rules/beyond-nesting-levels/) | [`TrackedResourceBeyondsThirdLevel`](https://github.com/Azure/azure-openapi-validator/blob/main/docs/tracked-resource-beyond-thrid-level.md) | **API.** RPC violation. | +| [`empty-updateable-properties`](https://azure.github.io/typespec-azure/docs/libraries/azure-resource-manager/rules/empty-updateable-properties/) | — | **API.** Covered by patch-specific rules. | +| [`improper-subscription-list-operation`](https://azure.github.io/typespec-azure/docs/libraries/azure-resource-manager/rules/improper-subscription-list-operation/) | — | **API.** Likely a modeling error; only subscription-based resources should have list operations. | +| [`list-operation-missing-pageable`](https://azure.github.io/typespec-azure/docs/libraries/azure-resource-manager/rules/list-operation-missing-pageable/) | [`XmsPageableForListCalls`](https://github.com/Azure/azure-openapi-validator/blob/main/docs/xms-pageable-for-list-calls.md) | **API, SDK.** Missing paging metadata makes generated list methods inconsistent and makes adding continuation links a breaking change. | +| [`list-response-envelope`](https://azure.github.io/typespec-azure/docs/libraries/azure-resource-manager/rules/list-response-envelope/) | [`CollectionObjectPropertiesNaming`](https://github.com/Azure/azure-openapi-validator/blob/main/docs/collection-object-properties-naming.md), [`GetCollectionOnlyHasValueAndNextLink`](https://github.com/Azure/azure-openapi-validator/blob/main/packages/rulesets/src/spectral/functions/get-collection-only-has-value-and-next-link.ts) | **API, SDK.** A nonstandard collection envelope or missing/non-array `value` property disrupts paged collection responses and client pagination. | +| [`lro-location-header`](https://azure.github.io/typespec-azure/docs/libraries/azure-resource-manager/rules/lro-location-header/) | [`LroLocationHeader`](https://github.com/Azure/azure-openapi-validator/blob/main/docs/lro-location-header.md) | **API.** RPC violation. | +| [`lro-missing-polling-metadata`](https://azure.github.io/typespec-azure/docs/libraries/azure-resource-manager/rules/lro-missing-polling-metadata/) | [`LroExtension`](https://github.com/Azure/azure-openapi-validator/blob/main/docs/lro-extension.md) (partial), [`XMSLongRunningOperationProperty`](https://github.com/Azure/azure-openapi-validator/blob/main/docs/xms-long-running-operation-property.md) | **API, SDK.** Require native polling metadata for PATCH/provider/collection POST `202` responses or PUT/PATCH/POST/DELETE `200`/`201`/`202`/`204` responses with polling headers. Disabled in the central resource-manager ruleset; included in ARM library `all`. | +| [`lro-response-mismatch`](https://azure.github.io/typespec-azure/docs/libraries/azure-resource-manager/rules/lro-response-mismatch/) | — | **API, SDK.** A final result that differs from the operation response can break generated long-running operation methods. | +| [`missing-operations-endpoint`](https://azure.github.io/typespec-azure/docs/libraries/azure-resource-manager/rules/missing-operations-endpoint/) | [R3023](https://github.com/Azure/azure-rest-api-specs/blob/main/documentation/openapi-authoring-automated-guidelines.md#r3023) | **API.** RPC violation. | +| [`missing-x-ms-identifiers`](https://azure.github.io/typespec-azure/docs/libraries/azure-resource-manager/rules/missing-x-ms-identifiers/) | [R4041](https://github.com/Azure/azure-rest-api-specs/blob/main/documentation/openapi-authoring-automated-guidelines.md#r4041) (warning) | **API.** No real impact (an old pattern). | +| [`no-empty-model`](https://azure.github.io/typespec-azure/docs/libraries/azure-resource-manager/rules/no-empty-model/) | [R4037](https://github.com/Azure/azure-rest-api-specs/blob/main/documentation/openapi-authoring-automated-guidelines.md#r4037) | **API, SDK.** Accepting any schema makes the API and SDK difficult to use. | +| [`no-override-props`](https://azure.github.io/typespec-azure/docs/libraries/azure-resource-manager/rules/no-override-props/) | — | **SDK, Tooling.** Violations can crash the breaking-change tool and are unsupported by most languages. | +| [`no-query-in-collection`](https://azure.github.io/typespec-azure/docs/libraries/azure-resource-manager/rules/no-query-in-collection/) | [`QueryParametersInCollectionGet`](https://github.com/Azure/azure-openapi-validator/blob/main/packages/rulesets/src/spectral/functions/query-parameters-in-collection-get.ts) | **API, SDK.** Nonstandard collection query parameters make list operations and generated SDK signatures inconsistent. | +| [`no-query-in-point-op`](https://azure.github.io/typespec-azure/docs/libraries/azure-resource-manager/rules/no-query-in-point-op/) | [`ValidQueryParametersForPointOperations`](https://github.com/Azure/azure-openapi-validator/blob/main/docs/valid-query-parameters-for-point-operations.md) | **API, SDK.** Point-operation query parameters beyond `api-version` violate the ARM request contract and complicate SDK signatures. | +| [`no-query-in-post`](https://azure.github.io/typespec-azure/docs/libraries/azure-resource-manager/rules/no-query-in-post/) | [`ParametersInPost`](https://github.com/Azure/azure-openapi-validator/blob/main/docs/parameters-in-post.md) | **API, SDK.** POST inputs outside the request body create nonstandard APIs and unstable SDK signatures. | +| [`no-resource-delete-operation`](https://azure.github.io/typespec-azure/docs/libraries/azure-resource-manager/rules/no-resource-delete-operation/) | [`AllTrackedResourcesMustHaveDelete`](https://github.com/Azure/azure-openapi-validator/blob/main/docs/all-tracked-resources-must-have-delete.md) | **API.** RPC violation. | +| [`no-response-body`](https://azure.github.io/typespec-azure/docs/libraries/azure-resource-manager/rules/no-response-body/) | — | **API.** A non-empty response, usually for a 202. | +| [`no-tenant-level-apis`](https://azure.github.io/typespec-azure/docs/libraries/azure-resource-manager/rules/no-tenant-level-apis/) | [`TenantLevelAPIsNotAllowed`](https://github.com/Azure/azure-openapi-validator/blob/main/docs/tenant-level-apis-not-allowed.md) | **API, Security.** Tenant-level PUT APIs bypass subscription and resource-group boundaries and require additional security review. | +| [`patch-envelope`](https://azure.github.io/typespec-azure/docs/libraries/azure-resource-manager/rules/patch-envelope/) | — | **API.** A Patch operation is missing updatable envelope properties. | +| [`resource-name`](https://azure.github.io/typespec-azure/docs/libraries/azure-resource-manager/rules/resource-name/) | — | **API, SDK.** Invalid characters in a name violate the RPC and create invalid client parameter names, which prevents SDK generation. | +| [`scope-path-segment-casing`](https://azure.github.io/typespec-azure/docs/libraries/azure-resource-manager/rules/scope-path-segment-casing/) | [`SubscriptionsAndResourceGroupCasing`](https://github.com/Azure/azure-openapi-validator/blob/main/docs/subscriptions-and-resource-group-casing.md) | **API, SDK.** Incorrect subscription and resource-group scope casing can prevent resource-provider registration and produce inconsistent client URLs. | +| [`secret-prop`](https://azure.github.io/typespec-azure/docs/libraries/azure-resource-manager/rules/secret-prop/) | [`XMSSecretInResponse`](https://github.com/Azure/azure-openapi-validator/blob/main/docs/xms-secret-in-response.md) | **API.** RPC violation. | +| [`unsupported-type`](https://azure.github.io/typespec-azure/docs/libraries/azure-resource-manager/rules/unsupported-type/) | — | **SDK.** The data type cannot be modeled in SDKs. | +| [`use-api-version`](https://azure.github.io/typespec-azure/docs/libraries/azure-resource-manager/rules/use-api-version/) | [`ApiVersionParameterRequired`](https://github.com/Azure/azure-openapi-validator/blob/main/docs/api-version-parameter-required.md) | **API, SDK.** An operation without the standard API version parameter cannot evolve safely and is difficult for SDKs to represent consistently. | +| [`use-application-json-content-type`](https://azure.github.io/typespec-azure/docs/libraries/azure-resource-manager/rules/use-application-json-content-type/) | [`NonApplicationJsonType`](https://github.com/Azure/azure-openapi-validator/blob/main/packages/rulesets/src/spectral/az-arm.ts) | **API, SDK.** Non-JSON request or response bodies are incompatible with the ARM contract and produce inconsistent serialization. | +| [`use-create-for-put`](https://azure.github.io/typespec-azure/docs/libraries/typespec-client-generator-core/rules/use-create-for-put/) | [PutInOperationName (R1006)](https://github.com/Azure/azure-rest-api-specs/blob/main/documentation/openapi-authoring-automated-guidelines.md#r1006) | **SDK.** TCGC checks the common SDK method name, including unscoped `@clientName` overrides; renaming a shipped method may break SDK compatibility. | +| [`use-interface`](https://azure.github.io/typespec-azure/docs/libraries/azure-resource-manager/rules/use-interface/) | — | **API, SDK, Emitters.** Resource operations outside interfaces can break ARM resource-operation modeling and downstream tooling assumptions. | +| [`use-model-request-body`](https://azure.github.io/typespec-azure/docs/libraries/azure-resource-manager/rules/use-model-request-body/) | [`ParametersSchemaAsTypeObject`](https://github.com/Azure/azure-openapi-validator/blob/main/docs/parameters-schema-as-type-object.md) | **API, SDK.** Non-model, array, and record request bodies cannot evolve with additional properties without a breaking change. | +| [`use-operation-decorator`](https://azure.github.io/typespec-azure/docs/libraries/azure-resource-manager/rules/use-operation-decorator/) | — | **API, SDK, Emitters.** Missing or mismatched decorators prevent resource operations from being associated with the correct resource, which can break SDK generation and resource-aware tooling. | +| [`version-progression`](https://azure.github.io/typespec-azure/docs/libraries/azure-resource-manager/rules/version-progression/) | — | **API, SDK, Tooling.** Out-of-order versions make specs unmaintainable, and versions with matching dates cause SDK problems. | ### Rules in @azure-tools/typespec-client-generator-core diff --git a/website/src/content/docs/docs/libraries/azure-resource-manager/reference/linter.md b/website/src/content/docs/docs/libraries/azure-resource-manager/reference/linter.md index 973963e572..5a06c890b7 100644 --- a/website/src/content/docs/docs/libraries/azure-resource-manager/reference/linter.md +++ b/website/src/content/docs/docs/libraries/azure-resource-manager/reference/linter.md @@ -60,7 +60,7 @@ Available ruleSets: | [`@azure-tools/typespec-azure-resource-manager/arm-resource-invalid-action-verb`](../rules/arm-resource-invalid-action-verb.md) | Actions must be HTTP Post or Get operations. | | [`@azure-tools/typespec-azure-resource-manager/improper-subscription-list-operation`](../rules/improper-subscription-list-operation.md) | Tenant and Extension resources should not define a list by subscription operation. | | [`@azure-tools/typespec-azure-resource-manager/lro-location-header`](../rules/lro-location-header.md) | A 202 response should include a Location response header. | -| [`@azure-tools/typespec-azure-resource-manager/lro-missing-polling-metadata`](../rules/lro-missing-polling-metadata.md) | ARM PATCH and provider/collection POST operations returning 202 must have native polling metadata. | +| [`@azure-tools/typespec-azure-resource-manager/lro-missing-polling-metadata`](../rules/lro-missing-polling-metadata.md) | ARM operations indicating asynchronous processing must have native polling metadata. | | [`@azure-tools/typespec-azure-resource-manager/list-operation-missing-pageable`](../rules/list-operation-missing-pageable.md) | ARM GET operations on list paths must define TypeSpec paging metadata. | | [`@azure-tools/typespec-azure-resource-manager/missing-x-ms-identifiers`](../rules/missing-x-ms-identifiers.md) | Array properties should describe their identifying properties with x-ms-identifiers. Decorate the property with @OpenAPI.extension("x-ms-identifiers", #[id-prop]) where "id-prop" is a list of the names of identifying properties in the item type. | | [`@azure-tools/typespec-azure-resource-manager/no-response-body`](../rules/no-response-body.md) | Check that the body is empty for 202 and 204 responses, and not empty for other success (2xx) responses. | From 2a67f1531e7a4faf6983c56be98ac36964b45f39 Mon Sep 17 00:00:00 2001 From: Yuchao Yan Date: Wed, 21 Oct 2026 12:11:12 +0800 Subject: [PATCH 7/8] refactor(arm): simplify LRO rule traversal and examples Use the native operation listener within explicit services, preserve concrete endpoint eligibility and resolver side effects, and consolidate duplicate shared-header coverage. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../lro-missing-polling-metadata-arm.md | 2 +- .../src/rules/lro-missing-polling-metadata.md | 43 ++++-------- .../src/rules/lro-missing-polling-metadata.ts | 70 ++++++++++++------- .../lro-missing-polling-metadata.test.ts | 51 ++++++++++---- 4 files changed, 96 insertions(+), 70 deletions(-) diff --git a/.chronus/changes/lro-missing-polling-metadata-arm.md b/.chronus/changes/lro-missing-polling-metadata-arm.md index aec2bcc23c..f4586b581f 100644 --- a/.chronus/changes/lro-missing-polling-metadata-arm.md +++ b/.chronus/changes/lro-missing-polling-metadata-arm.md @@ -6,6 +6,6 @@ packages: Add the `lro-missing-polling-metadata` lint rule, consolidating `LroExtension` and `XMSLongRunningOperationProperty`. Require native polling metadata for PATCH and marked provider/collection POST operations returning `202`, and for PUT/PATCH/POST/DELETE operations whose `200`, `201`, `202`, or `204` responses contain `Location` or `Azure-AsyncOperation` headers. -Use asynchronous ARM templates or preserve supported semantic LRO headers. Report once on the authored operation even when both checks apply, and resolve each eligible operation once across overlapping service declarations, including compliant operations. +Use asynchronous ARM templates or preserve supported semantic LRO headers. Check concrete operations within explicit service namespaces, including nested namespaces, without a global fallback. Report once on the authored operation even when both checks apply, and resolve each eligible operation once across overlapping service declarations, including compliant operations. The rule is disabled in the central resource-manager ruleset and included in the ARM library's compiler-generated `all` ruleset. diff --git a/packages/typespec-azure-resource-manager/src/rules/lro-missing-polling-metadata.md b/packages/typespec-azure-resource-manager/src/rules/lro-missing-polling-metadata.md index 4a7354d5d1..ca2aecad75 100644 --- a/packages/typespec-azure-resource-manager/src/rules/lro-missing-polling-metadata.md +++ b/packages/typespec-azure-resource-manager/src/rules/lro-missing-polling-metadata.md @@ -28,9 +28,10 @@ Registered resource-instance POST, PUT, and DELETE operations remain subject to the polling-header check. Existing ARM response-code and Location-header rules retain ownership of their separate status-code and header-presence requirements. -Operations reached through overlapping service declarations are checked once, -including compliant operations. This avoids repeating diagnostics from native -polling-metadata resolution. +Each concrete operation within an explicitly declared service is checked once, +including operations in nested namespaces and overlapping services. Operations +outside explicit services are not checked. This avoids repeating diagnostics +from native polling-metadata resolution. The combined rule retains one disabled (`false`) entry in the central Azure resource-manager ruleset. The ARM library's compiler-generated `all` ruleset @@ -49,25 +50,16 @@ completion. ## ❌ Incorrect Replacing the standard headers with a string-valued `Location` header preserves -the accepted response but removes its polling semantics: +the accepted response but removes its polling semantics. These excerpts assume +the `Widget` resource model is already defined: ```tsp -@armProviderNamespace -namespace Microsoft.Contoso; - -model Widget is TrackedResource<{}> { - ...ResourceNameParameter; -} - model PlainLocationHeaders { ...Azure.Core.Foundations.RetryAfterHeader; @header("Location") location?: string; } -@armResourceOperations -interface Widgets { - update is ArmCustomPatchAsync; -} +op update is ArmCustomPatchAsync; ``` ## ✅ Correct @@ -76,22 +68,11 @@ Keep the same asynchronous operation and accepted response, but restore the supported polling and final-result metadata: ```tsp -@armProviderNamespace -namespace Microsoft.Contoso; - -model Widget is TrackedResource<{}> { - ...ResourceNameParameter; -} - -@armResourceOperations -interface Widgets { - update is ArmCustomPatchAsync< - Widget, - Widget, - LroHeaders = ArmLroLocationHeader & - Azure.Core.Foundations.RetryAfterHeader - >; -} +op update is ArmCustomPatchAsync< + Widget, + Widget, + LroHeaders = ArmLroLocationHeader & Azure.Core.Foundations.RetryAfterHeader +>; ``` Custom `ArmProviderActionAsync` headers have the same requirement. The default diff --git a/packages/typespec-azure-resource-manager/src/rules/lro-missing-polling-metadata.ts b/packages/typespec-azure-resource-manager/src/rules/lro-missing-polling-metadata.ts index e3cbf4e7b7..91fe7ac6f5 100644 --- a/packages/typespec-azure-resource-manager/src/rules/lro-missing-polling-metadata.ts +++ b/packages/typespec-azure-resource-manager/src/rules/lro-missing-polling-metadata.ts @@ -1,6 +1,12 @@ import { getLroMetadata } from "@azure-tools/typespec-azure-core"; -import { createRule, fileRef, listServices, type Operation } from "@typespec/compiler"; -import { getHttpService, type HttpOperationResponse } from "@typespec/http"; +import { + createRule, + fileRef, + isTemplateDeclaration, + isTemplateDeclarationOrInstance, + listServices, +} from "@typespec/compiler"; +import { getHttpOperation, type HttpOperationResponse } from "@typespec/http"; import { isArmCollectionAction } from "../operations.js"; const pollingStatusCodes = new Set([200, 201, 202, 204]); @@ -19,31 +25,43 @@ export const lroMissingPollingMetadataRule = createRule({ "ARM operations indicating asynchronous processing must have native polling metadata. Use an asynchronous ARM template or preserve supported semantic LRO headers.", }, create(context) { + const serviceNamespaces = new Set(listServices(context.program).map((service) => service.type)); return { - root() { - const visitedOperations = new Set(); - for (const service of listServices(context.program)) { - const [httpService] = getHttpService(context.program, service.type); - for (const httpOperation of httpService.operations) { - const operation = httpOperation.operation; - if (visitedOperations.has(operation)) { - continue; - } - const requiresAcceptedMetadata = - httpOperation.verb === "patch" || - (httpOperation.verb === "post" && isArmCollectionAction(context.program, operation)); - const hasAcceptedResponse = - requiresAcceptedMetadata && - httpOperation.responses.some((response) => response.statusCodes === 202); - const hasPollingResponse = - pollingVerbs.has(httpOperation.verb) && hasPollingHeader(httpOperation.responses); - if (hasAcceptedResponse || hasPollingResponse) { - // Metadata resolution can report Core diagnostics even for compliant operations. - visitedOperations.add(operation); - if (getLroMetadata(context.program, operation) === undefined) { - context.reportDiagnostic({ target: operation }); - } - } + operation(operation) { + const container = operation.interface ?? operation.namespace; + if ( + isTemplateDeclarationOrInstance(operation) || + !container || + container.operations.get(operation.name) !== operation || + (operation.interface && + (isTemplateDeclaration(operation.interface) || + operation.interface.namespace?.interfaces.get(operation.interface.name) !== + operation.interface)) + ) { + return; + } + + let namespace = operation.namespace; + while (namespace && !serviceNamespaces.has(namespace)) { + namespace = namespace.namespace; + } + if (!namespace) { + return; + } + + const [httpOperation] = getHttpOperation(context.program, operation); + const requiresAcceptedMetadata = + httpOperation.verb === "patch" || + (httpOperation.verb === "post" && isArmCollectionAction(context.program, operation)); + const hasAcceptedResponse = + requiresAcceptedMetadata && + httpOperation.responses.some((response) => response.statusCodes === 202); + const hasPollingResponse = + pollingVerbs.has(httpOperation.verb) && hasPollingHeader(httpOperation.responses); + if (hasAcceptedResponse || hasPollingResponse) { + // Metadata resolution can report Core diagnostics even for compliant operations. + if (getLroMetadata(context.program, operation) === undefined) { + context.reportDiagnostic({ target: operation }); } } }, diff --git a/packages/typespec-azure-resource-manager/test/rules/lro-missing-polling-metadata.test.ts b/packages/typespec-azure-resource-manager/test/rules/lro-missing-polling-metadata.test.ts index 60f53bbeb6..63318e1867 100644 --- a/packages/typespec-azure-resource-manager/test/rules/lro-missing-polling-metadata.test.ts +++ b/packages/typespec-azure-resource-manager/test/rules/lro-missing-polling-metadata.test.ts @@ -207,23 +207,50 @@ it("reports a nested service operation once across overlapping HTTP service trav })); }); -it("reports each distinct provider operation once when response headers are shared", async () => { +it.each(["", "namespace Unscoped;"])( + "does not fall back to operations without an explicit service (%s)", + async (namespace) => { + await tester + .expect( + ` + ${namespace} + @patch op update(): { @statusCode statusCode: 202; @header("Location") location?: string; }; + `, + ) + .toBeValid(); + }, +); + +it("ignores operations outside the explicit service", async () => { await tester .expect( - `${header} - @armResourceOperations - interface ProviderOperations { - /*start*/startProvider is ArmProviderActionAsync< - Request = void, Response = void, LroHeaders = PlainLocationHeaders - >; - /*stop*/stopProvider is ArmProviderActionAsync< - Request = void, Response = void, LroHeaders = PlainLocationHeaders - >; + ` + @service namespace SelectedArmProgram {} + namespace Outside { + @patch op update(): { @statusCode statusCode: 202; @header("Location") location?: string; }; + } + `, + ) + .toBeValid(); +}); + +it("checks concrete endpoints in nested namespaces but not templates or their instances", async () => { + await tester + .expect( + ` + @service namespace SelectedArmProgram; + model Accepted { @statusCode statusCode: 202; } + @patch op update(): Accepted; + interface Template { @patch update(): Accepted; } + alias Instance = Template; + namespace Nested { + @route("/one") op /*first*/first is update; + @route("/two") op /*second*/second is Instance.update; } `, ) - .toEmitDiagnostics(({ start, stop }) => - [start, stop].map((operation) => ({ + .toEmitDiagnostics(({ first, second }) => + [first, second].map((operation) => ({ ...diagnostic, pos: getSourceLocation(operation).pos, end: getSourceLocation(operation).end, From 09b7f5127464500002f8845941148d78b08d68c9 Mon Sep 17 00:00:00 2001 From: Yuchao Yan Date: Wed, 21 Oct 2026 18:00:26 +0800 Subject: [PATCH 8/8] test(arm): simplify polling metadata acceptance assertions Use observable native async positives and matched plain-header negatives without HTTP metadata assertions. Retain the registered resolver-side-effect regression. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../lro-missing-polling-metadata.test.ts | 44 ++++++++----------- 1 file changed, 19 insertions(+), 25 deletions(-) diff --git a/packages/typespec-azure-resource-manager/test/rules/lro-missing-polling-metadata.test.ts b/packages/typespec-azure-resource-manager/test/rules/lro-missing-polling-metadata.test.ts index 63318e1867..e352dd4d17 100644 --- a/packages/typespec-azure-resource-manager/test/rules/lro-missing-polling-metadata.test.ts +++ b/packages/typespec-azure-resource-manager/test/rules/lro-missing-polling-metadata.test.ts @@ -3,11 +3,9 @@ import { getSourceLocation } from "@typespec/compiler"; import { createLinterRuleTester, expectDiagnostics, - t, type LinterRuleTester, } from "@typespec/compiler/testing"; -import { getHttpOperation } from "@typespec/http"; -import { beforeEach, describe, expect, it } from "vitest"; +import { beforeEach, describe, it } from "vitest"; import { lroMissingPollingMetadataRule } from "../../src/rules/lro-missing-polling-metadata.js"; const header = ` @@ -106,33 +104,29 @@ it("reports an async provider POST with plain Location headers", async () => { it("accepts standard async resource operations with qualifying polling responses", async () => { await tester .expect( - t.code`${header} + `${header} @armResourceOperations interface Widgets { - ${t.op("createOrUpdate")} is ArmResourceCreateOrReplaceAsync; - ${t.op("update")} is ArmResourcePatchAsync; - ${t.op("remove")} is ArmResourceDeleteWithoutOkAsync; + createOrUpdate is ArmResourceCreateOrReplaceAsync; + update is ArmResourcePatchAsync; + remove is ArmResourceDeleteWithoutOkAsync; } `, ) - .toEmitDiagnostics(({ program, createOrUpdate, update, remove }) => { - for (const operation of [createOrUpdate, update, remove]) { - const [httpOperation] = getHttpOperation(program, operation); - expect( - httpOperation.responses.some( - (response) => - typeof response.statusCodes === "number" && - [200, 201, 202, 204].includes(response.statusCodes) && - response.responses.some((content) => - Object.keys(content.headers ?? {}).some((name) => - ["location", "azure-asyncoperation"].includes(name.toLowerCase()), - ), - ), - ), - ).toBe(true); - } - return []; - }); + .toBeValid(); +}); + +it.each([ + [ + "PATCH", + "update is ArmResourcePatchAsync;", + ], + [ + "DELETE", + "remove is ArmResourceDeleteWithoutOkAsync;", + ], +])("reports the same async resource %s with plain polling headers", async (_verb, operation) => { + await expectViolation(operation, "Widgets"); }); it("accepts default async custom PATCH polling metadata", async () => {