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..f4586b581f --- /dev/null +++ b/.chronus/changes/lro-missing-polling-metadata-arm.md @@ -0,0 +1,11 @@ +--- +changeKind: feature +packages: + - "@azure-tools/typespec-azure-resource-manager" +--- + +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. 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/.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 a3468caa7c..f61759c764 100644 --- a/packages/typespec-azure-resource-manager/README.md +++ b/packages/typespec-azure-resource-manager/README.md @@ -66,6 +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 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/linter.ts b/packages/typespec-azure-resource-manager/src/linter.ts index 7749a112a7..5af6bfc920 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"; @@ -98,6 +99,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..ca2aecad75 --- /dev/null +++ b/packages/typespec-azure-resource-manager/src/rules/lro-missing-polling-metadata.md @@ -0,0 +1,109 @@ +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 +`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. + +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. + +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 +includes registered rules, including this one. Consolidation adds no second +public rule or registration; selecting this rule selects both checks. + +## 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. These excerpts assume +the `Widget` resource model is already defined: + +```tsp +model PlainLocationHeaders { + ...Azure.Core.Foundations.RetryAfterHeader; + @header("Location") location?: string; +} + +op update is ArmCustomPatchAsync; +``` + +## ✅ Correct + +Keep the same asynchronous operation and accepted response, but restore the +supported polling and final-result metadata: + +```tsp +op 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 + +| 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 new file mode 100644 index 0000000000..91fe7ac6f5 --- /dev/null +++ b/packages/typespec-azure-resource-manager/src/rules/lro-missing-polling-metadata.ts @@ -0,0 +1,81 @@ +import { getLroMetadata } from "@azure-tools/typespec-azure-core"; +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]); +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 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 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 { + 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 }); + } + } + }, + }; + }, +}); + +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/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 new file mode 100644 index 0000000000..e352dd4d17 --- /dev/null +++ b/packages/typespec-azure-resource-manager/test/rules/lro-missing-polling-metadata.test.ts @@ -0,0 +1,515 @@ +import { Tester } from "#test/tester.js"; +import { getSourceLocation } from "@typespec/compiler"; +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 = ` +@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, +}; + +let tester: LinterRuleTester; + +beforeEach(async () => { + const rawTester = await Tester.createInstance(); + tester = createLinterRuleTester( + rawTester, + 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 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 () => { + 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 operations with qualifying polling responses", async () => { + await tester + .expect( + `${header} + @armResourceOperations + interface Widgets { + createOrUpdate is ArmResourceCreateOrReplaceAsync; + update is ArmResourcePatchAsync; + remove is ArmResourceDeleteWithoutOkAsync; + } + `, + ) + .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 () => { + await expectCompliant("update is ArmCustomPatchAsync;", "Widgets"); +}); + +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 () => { + 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.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( + ` + @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(({ first, second }) => + [first, second].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("reports polling headers on registered resource-instance POST operations", async () => { + await expectViolation( + "startWidget is ArmResourceActionSync>;", + "Widgets", + ); +}); + +it("ignores GET PUT and DELETE accepted responses without polling headers", async () => { + await tester + .expect( + ` + @service namespace SelectedArmProgram; + model Accepted { @statusCode statusCode: 202; } + @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 without polling headers", async () => { + await tester + .expect( + ` + @service namespace SelectedArmProgram; + 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( + ` + @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-rulesets/src/rulesets/resource-manager.ts b/packages/typespec-azure-rulesets/src/rulesets/resource-manager.ts index 62f93a688a..c1da4192b5 100644 --- a/packages/typespec-azure-rulesets/src/rulesets/resource-manager.ts +++ b/packages/typespec-azure-rulesets/src/rulesets/resource-manager.ts @@ -100,6 +100,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/packages/typespec-azure-rulesets/test/validate-rules-defined.test.ts b/packages/typespec-azure-rulesets/test/validate-rules-defined.test.ts index 81346f615e..757a1586ff 100644 --- a/packages/typespec-azure-rulesets/test/validate-rules-defined.test.ts +++ b/packages/typespec-azure-rulesets/test/validate-rules-defined.test.ts @@ -107,4 +107,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 4e4f5eb0cc..0cde3ac5c6 100644 --- a/website/src/content/docs/docs/howtos/ARM/arm-rules.md +++ b/website/src/content/docs/docs/howtos/ARM/arm-rules.md @@ -97,62 +97,63 @@ 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-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-tags-on-proxy-resource`](https://azure.github.io/typespec-azure/docs/libraries/azure-resource-manager/rules/no-tags-on-proxy-resource/) | [`TagsAreNotAllowedForProxyResources`](https://github.com/Azure/azure-openapi-validator/blob/main/docs/tags-are-not-allowed-for-proxy-resources.md) | **API.** Envelope tags on non-tracked resources incorrectly imply ARM tagging support. | -| [`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-missing-read`](https://azure.github.io/typespec-azure/docs/libraries/azure-resource-manager/rules/resource-missing-read/) | [`AllResourcesMustHaveGetOperation`](https://github.com/Azure/azure-openapi-validator/blob/main/docs/all-resources-must-have-get-operation.md) | **API.** Clients cannot retrieve the state of a resource they can create or update. | -| [`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-resource-path-for-put`](https://azure.github.io/typespec-azure/docs/libraries/azure-resource-manager/rules/use-resource-path-for-put/) | [EvenSegmentedPathForPutOperation](https://github.com/Azure/azure-openapi-validator/blob/main/docs/even-segmented-path-for-put-operation.md) | **API, SDK.** PUT paths must identify a resource instance using resource type/name pairs. Disabled by default in the resource-manager ruleset. | -| [`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. | -| [`use-scope-for-extension-routing`](https://azure.github.io/typespec-azure/docs/libraries/azure-resource-manager/rules/use-scope-for-extension-routing/) | [`ExtensionResourcePathPattern`](https://github.com/Azure/azure-openapi-validator/blob/main/docs/extension-resource-path-pattern.md) | **API, SDK.** Opt-in for managed RPaaS routing; not enabled by ARM `all` or the general resource-manager ruleset. Ordinary ARM specific-parent routes remain supported. | -| [`use-standard-response-codes`](https://azure.github.io/typespec-azure/docs/libraries/azure-resource-manager/rules/use-standard-response-codes/) | [NoErrorCodeResponses](https://github.com/Azure/azure-openapi-validator/blob/main/packages/rulesets/src/spectral/functions/no-error-code-responses.ts), [GetResponseCodes](https://github.com/Azure/azure-openapi-validator/blob/main/docs/get-response-codes.md) | **API, SDK.** Use only `200`, `201`, `202`, `204`, or default; GET operations inside services must include `200` and may only add `202` or default. Disabled by default. | -| [`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-tags-on-proxy-resource`](https://azure.github.io/typespec-azure/docs/libraries/azure-resource-manager/rules/no-tags-on-proxy-resource/) | [`TagsAreNotAllowedForProxyResources`](https://github.com/Azure/azure-openapi-validator/blob/main/docs/tags-are-not-allowed-for-proxy-resources.md) | **API.** Envelope tags on non-tracked resources incorrectly imply ARM tagging support. | +| [`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-missing-read`](https://azure.github.io/typespec-azure/docs/libraries/azure-resource-manager/rules/resource-missing-read/) | [`AllResourcesMustHaveGetOperation`](https://github.com/Azure/azure-openapi-validator/blob/main/docs/all-resources-must-have-get-operation.md) | **API.** Clients cannot retrieve the state of a resource they can create or update. | +| [`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-resource-path-for-put`](https://azure.github.io/typespec-azure/docs/libraries/azure-resource-manager/rules/use-resource-path-for-put/) | [EvenSegmentedPathForPutOperation](https://github.com/Azure/azure-openapi-validator/blob/main/docs/even-segmented-path-for-put-operation.md) | **API, SDK.** PUT paths must identify a resource instance using resource type/name pairs. Disabled by default in the resource-manager ruleset. | +| [`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. | +| [`use-scope-for-extension-routing`](https://azure.github.io/typespec-azure/docs/libraries/azure-resource-manager/rules/use-scope-for-extension-routing/) | [`ExtensionResourcePathPattern`](https://github.com/Azure/azure-openapi-validator/blob/main/docs/extension-resource-path-pattern.md) | **API, SDK.** Opt-in for managed RPaaS routing; not enabled by ARM `all` or the general resource-manager ruleset. Ordinary ARM specific-parent routes remain supported. | +| [`use-standard-response-codes`](https://azure.github.io/typespec-azure/docs/libraries/azure-resource-manager/rules/use-standard-response-codes/) | [NoErrorCodeResponses](https://github.com/Azure/azure-openapi-validator/blob/main/packages/rulesets/src/spectral/functions/no-error-code-responses.ts), [GetResponseCodes](https://github.com/Azure/azure-openapi-validator/blob/main/docs/get-response-codes.md) | **API, SDK.** Use only `200`, `201`, `202`, `204`, or default; GET operations inside services must include `200` and may only add `202` or default. Disabled by default. | +| [`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 95df812776..54e14353c7 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,6 +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 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. |