diff --git a/.chronus/changes/promote-patch-body-parameters-schema-2026-08-25-05-25-55.md b/.chronus/changes/promote-patch-body-parameters-schema-2026-08-25-05-25-55.md new file mode 100644 index 0000000000..20b6048601 --- /dev/null +++ b/.chronus/changes/promote-patch-body-parameters-schema-2026-08-25-05-25-55.md @@ -0,0 +1,7 @@ +--- +changeKind: feature +packages: + - "@azure-tools/typespec-azure-resource-manager" +--- + +Add the `no-unsafe-patch-body-properties` ARM linter rule to report required, default-valued, and create-only properties emitted in PATCH request bodies. diff --git a/.chronus/changes/register-no-unsafe-patch-body-properties-ruleset.md b/.chronus/changes/register-no-unsafe-patch-body-properties-ruleset.md new file mode 100644 index 0000000000..9772186386 --- /dev/null +++ b/.chronus/changes/register-no-unsafe-patch-body-properties-ruleset.md @@ -0,0 +1,7 @@ +--- +changeKind: internal +packages: + - "@azure-tools/typespec-azure-rulesets" +--- + +Register the ARM `no-unsafe-patch-body-properties` 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 985a806392..73b5fb3948 100644 --- a/packages/typespec-azure-resource-manager/README.md +++ b/packages/typespec-azure-resource-manager/README.md @@ -64,6 +64,7 @@ Available ruleSets: | [`@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. | | [`@azure-tools/typespec-azure-resource-manager/missing-operations-endpoint`](https://azure.github.io/typespec-azure/docs/libraries/azure-resource-manager/rules/missing-operations-endpoint) | Check for missing Operations interface. | +| [`@azure-tools/typespec-azure-resource-manager/no-unsafe-patch-body-properties`](https://azure.github.io/typespec-azure/docs/libraries/azure-resource-manager/rules/no-unsafe-patch-body-properties) | ARM PATCH body properties must not be required, have defaults, or be create-only. | | [`@azure-tools/typespec-azure-resource-manager/patch-envelope`](https://azure.github.io/typespec-azure/docs/libraries/azure-resource-manager/rules/patch-envelope) | Patch envelope properties should match the resource properties. | | [`@azure-tools/typespec-azure-resource-manager/arm-resource-patch`](https://azure.github.io/typespec-azure/docs/libraries/azure-resource-manager/rules/arm-resource-patch) | Validate ARM PATCH operations. | | [`@azure-tools/typespec-azure-resource-manager/resource-name`](https://azure.github.io/typespec-azure/docs/libraries/azure-resource-manager/rules/resource-name) | Check the resource name. | diff --git a/packages/typespec-azure-resource-manager/src/linter.ts b/packages/typespec-azure-resource-manager/src/linter.ts index 671fdbea61..06e2c21c5b 100644 --- a/packages/typespec-azure-resource-manager/src/linter.ts +++ b/packages/typespec-azure-resource-manager/src/linter.ts @@ -32,6 +32,7 @@ import { noOverridePropsRule } from "./rules/no-override-props.js"; import { noReservedResourcePropertyRule } from "./rules/no-reserved-resource-property.js"; import { deleteOperationMissingRule } from "./rules/no-resource-delete-operation.js"; import { noResponseBodyRule } from "./rules/no-response-body.js"; +import { noUnsafePatchBodyPropertiesRule } from "./rules/no-unsafe-patch-body-properties.js"; import { operationsInterfaceMissingRule } from "./rules/operations-interface-missing.js"; import { patchEnvelopePropertiesRules } from "./rules/patch-envelope-properties.js"; import { resourceNameRule } from "./rules/resource-name.js"; @@ -81,6 +82,7 @@ const rules = [ missingXmsIdentifiersRule, noResponseBodyRule, operationsInterfaceMissingRule, + noUnsafePatchBodyPropertiesRule, patchEnvelopePropertiesRules, patchOperationsRule, resourceNameRule, diff --git a/packages/typespec-azure-resource-manager/src/rules/no-unsafe-patch-body-properties.md b/packages/typespec-azure-resource-manager/src/rules/no-unsafe-patch-body-properties.md new file mode 100644 index 0000000000..7749e854ef --- /dev/null +++ b/packages/typespec-azure-resource-manager/src/rules/no-unsafe-patch-body-properties.md @@ -0,0 +1,52 @@ +ARM PATCH request body properties must be safe for partial updates. A property emitted in an ARM PATCH body must not be required, must not define a default value, and must not be create-only. + +## Impact + +- **Area:** API + +PATCH describes partial updates. Required PATCH body properties, default-valued properties, and create-only properties can make partial updates ambiguous for service authors and SDKs, and can produce ARM OpenAPI that violates PATCH request-body guidance. + +The rule checks the effective emitted PATCH payload. Properties omitted from the PATCH payload, such as `never` properties or create-only properties removed by the PATCH visibility transform, are not reported. A top-level emitted property named `identity` is skipped to match ARM PATCH identity envelope behavior. + +## ❌ Incorrect + +```tsp +@armProviderNamespace +namespace Microsoft.Contoso; + +model WidgetPatchBody { + displayName: string; + enabled?: boolean = false; + + @visibility(Lifecycle.Create) + createdBy?: string; +} + +@route("/widgets/{name}") +@patch +op update(@path name: string, @body body: WidgetPatchBody): void; +``` + +## ✅ Correct + +```tsp +@armProviderNamespace +namespace Microsoft.Contoso; + +model WidgetPatchBody { + displayName?: string; + enabled?: boolean; +} + +@route("/widgets/{name}") +@patch +op update(@path name: string, @body body: WidgetPatchBody): void; +``` + +## LintDiff Equivalent + +This rule corresponds to the LintDiff rule [PatchBodyParametersSchema](https://github.com/Azure/azure-openapi-validator/blob/main/docs/patch-body-parameters-schema.md). + +## Suppression + +Do not suppress this rule for ordinary ARM resource PATCH operations. Fix the PATCH model so updateable properties are optional, do not carry defaults, and exclude create-only properties from the emitted PATCH payload. diff --git a/packages/typespec-azure-resource-manager/src/rules/no-unsafe-patch-body-properties.ts b/packages/typespec-azure-resource-manager/src/rules/no-unsafe-patch-body-properties.ts new file mode 100644 index 0000000000..0d9279c10e --- /dev/null +++ b/packages/typespec-azure-resource-manager/src/rules/no-unsafe-patch-body-properties.ts @@ -0,0 +1,296 @@ +import { + createRule, + fileRef, + getDiscriminator, + getLifecycleVisibilityEnum, + getLocationContext, + getVisibilityForClass, + isNeverType, + isNullType, + paramMessage, + resolveEncodedName, + type DiagnosticTarget, + type Model, + type ModelProperty, + type Operation, + type Program, + type Type, +} from "@typespec/compiler"; +import { + createMetadataInfo, + getHttpOperation, + resolveRequestVisibility, + Visibility, + type MetadataInfo, +} from "@typespec/http"; +import { resolveProviderNamespace } from "../namespace.js"; + +export const noUnsafePatchBodyPropertiesRule = createRule({ + name: "no-unsafe-patch-body-properties", + docs: fileRef.fromPackageRoot("src/rules/no-unsafe-patch-body-properties.md"), + severity: "warning", + description: "ARM PATCH body properties must not be required, have defaults, or be create-only.", + url: "https://azure.github.io/typespec-azure/docs/libraries/azure-resource-manager/rules/no-unsafe-patch-body-properties", + messages: { + required: paramMessage`Properties of a PATCH request body must not be required, property:${"propertyName"}.`, + default: paramMessage`Properties of a PATCH request body must not have default value, property:${"propertyName"}.`, + createOnly: paramMessage`Properties of a PATCH request body must not be x-ms-mutability: ["create"], property:${"propertyName"}.`, + }, + create(context) { + return { + operation: (operation) => { + const namespace = operation.interface?.namespace ?? operation.namespace; + if (resolveProviderNamespace(context.program, namespace) === undefined) { + return; + } + + const [httpOperation] = getHttpOperation(context.program, operation); + if (httpOperation.verb !== "patch") { + return; + } + + const patchBody = httpOperation.parameters.body?.type; + if (patchBody === undefined) { + return; + } + + for (const violation of findViolations(context.program, patchBody, operation)) { + context.reportDiagnostic({ + target: violation.target, + messageId: violation.messageId, + format: { + propertyName: violation.propertyName, + }, + }); + } + }, + }; + }, +}); + +type Violation = { + target: DiagnosticTarget; + propertyName: string; + messageId: "required" | "default" | "createOnly"; +}; + +function findViolations(program: Program, patchBody: Type, operation: Operation): Violation[] { + const violations: Violation[] = []; + const metadataInfo = createMetadataInfo(program, { + canonicalVisibility: Visibility.Read, + canShareProperty: (property) => canSharePropertyUsingReadonlyOrXmsMutability(program, property), + }); + const visibility = resolveRequestVisibility(program, operation, "patch"); + collectNestedViolations( + program, + patchBody, + violations, + [], + new Map(), + operation, + metadataInfo, + visibility, + ); + return violations; +} + +function collectViolations( + program: Program, + model: Model, + violations: Violation[], + path: string[] = [], + visited: Map> = new Map(), + diagnosticTarget: DiagnosticTarget, + metadataInfo: MetadataInfo, + visibility: Visibility, +) { + const schemaVisibility = metadataInfo.isTransformed(model, visibility) + ? visibility + : Visibility.Read; + const visitedVisibilities = visited.get(model); + if (visitedVisibilities?.has(schemaVisibility)) { + return; + } + if (visitedVisibilities === undefined) { + visited.set(model, new Set([schemaVisibility])); + } else { + visitedVisibilities.add(schemaVisibility); + } + + const discriminator = getInheritedDiscriminator(program, model); + if ( + discriminator !== undefined && + getModelProperty(model, discriminator.propertyName) === undefined && + !isTopLevelIdentityProperty([...path, discriminator.propertyName], discriminator.propertyName) + ) { + violations.push({ + target: getLocationContext(program, model).type === "project" ? model : diagnosticTarget, + propertyName: [...path, discriminator.propertyName].join("."), + messageId: "required", + }); + } + + for (const property of getModelProperties(model)) { + const jsonName = resolveEncodedName(program, property, "application/json"); + const propertyPath = [...path, jsonName]; + if (isTopLevelIdentityProperty(propertyPath, jsonName)) { + continue; + } + if (!metadataInfo.isPayloadProperty(property, schemaVisibility)) { + continue; + } + if (isNeverType(property.type)) { + continue; + } + const propertyTarget = + getLocationContext(program, property).type === "project" ? property : diagnosticTarget; + + if ( + !metadataInfo.isOptional(property, schemaVisibility) || + property.name === discriminator?.propertyName + ) { + violations.push({ + target: propertyTarget, + propertyName: propertyPath.join("."), + messageId: "required", + }); + } + + if (property.defaultValue !== undefined) { + violations.push({ + target: propertyTarget, + propertyName: propertyPath.join("."), + messageId: "default", + }); + } + + if (isCreateOnlyMutability(program, property)) { + violations.push({ + target: propertyTarget, + propertyName: propertyPath.join("."), + messageId: "createOnly", + }); + } + + collectNestedViolations( + program, + property.type, + violations, + propertyPath, + visited, + propertyTarget, + metadataInfo, + schemaVisibility, + ); + } +} + +function collectNestedViolations( + program: Program, + type: Type, + violations: Violation[], + path: string[], + visited: Map>, + diagnosticTarget: DiagnosticTarget, + metadataInfo: MetadataInfo, + visibility: Visibility, +) { + if (type.kind === "Model") { + collectViolations( + program, + type, + violations, + path, + visited, + diagnosticTarget, + metadataInfo, + visibility, + ); + return; + } + + if (type.kind === "Union") { + const nonNullVariants = [...type.variants.values()] + .map((variant) => variant.type) + .filter((variant) => !isNullType(variant)); + if (nonNullVariants.length === 1) { + collectNestedViolations( + program, + nonNullVariants[0], + violations, + path, + visited, + diagnosticTarget, + metadataInfo, + visibility, + ); + } + } +} + +function isTopLevelIdentityProperty(propertyPath: string[], jsonName: string): boolean { + return propertyPath.length === 1 && jsonName.toLowerCase() === "identity"; +} + +function isCreateOnlyMutability(program: Program, property: ModelProperty): boolean { + const lifecycle = getLifecycleVisibilityEnum(program); + const create = lifecycle.members.get("Create"); + if (create === undefined) { + return false; + } + + const visibility = getVisibilityForClass(program, property, lifecycle); + return visibility.size === 1 && visibility.has(create); +} + +function getInheritedDiscriminator(program: Program, model: Model) { + for (let current: Model | undefined = model; current !== undefined; current = current.baseModel) { + const discriminator = getDiscriminator(program, current); + if (discriminator !== undefined) { + return discriminator; + } + } + + return undefined; +} + +function canSharePropertyUsingReadonlyOrXmsMutability( + program: Program, + property: ModelProperty, +): boolean { + const lifecycle = getLifecycleVisibilityEnum(program); + const visibility = getVisibilityForClass(program, property, lifecycle); + if (visibility.size === lifecycle.members.size) { + return true; + } + + return ( + visibility.size > 0 && + [...visibility].every((member) => ["Read", "Create", "Update"].includes(member.name)) + ); +} + +function getModelProperty(model: Model, name: string): ModelProperty | undefined { + for (let current: Model | undefined = model; current !== undefined; current = current.baseModel) { + const property = current.properties.get(name); + if (property !== undefined) { + return property; + } + } + + return undefined; +} + +function getModelProperties(model: Model): ModelProperty[] { + const properties = new Map(); + + for (let current: Model | undefined = model; current !== undefined; current = current.baseModel) { + for (const property of current.properties.values()) { + if (!properties.has(property.name)) { + properties.set(property.name, property); + } + } + } + + return [...properties.values()]; +} diff --git a/packages/typespec-azure-resource-manager/test/rules/no-unsafe-patch-body-properties.test.ts b/packages/typespec-azure-resource-manager/test/rules/no-unsafe-patch-body-properties.test.ts new file mode 100644 index 0000000000..c8ac622b90 --- /dev/null +++ b/packages/typespec-azure-resource-manager/test/rules/no-unsafe-patch-body-properties.test.ts @@ -0,0 +1,354 @@ +import { Tester } from "#test/tester.js"; +import { + type LinterRuleTester, + type TesterInstance, + createLinterRuleTester, +} from "@typespec/compiler/testing"; +import { beforeEach, describe, it } from "vitest"; +import { noUnsafePatchBodyPropertiesRule } from "../../src/rules/no-unsafe-patch-body-properties.js"; + +const ruleCode = "@azure-tools/typespec-azure-resource-manager/no-unsafe-patch-body-properties"; + +let runner: TesterInstance; +let tester: LinterRuleTester; + +beforeEach(async () => { + runner = await Tester.createInstance(); + tester = createLinterRuleTester( + runner, + noUnsafePatchBodyPropertiesRule, + "@azure-tools/typespec-azure-resource-manager", + ); +}); + +function patchOperation(bodyType: string): string { + return ` + @armProviderNamespace + namespace Microsoft.TestService; + + @route("/widgets/{name}") + @patch + op update(@path name: string, @body body: ${bodyType}): void; + `; +} + +describe("invalid cases", () => { + it("emits diagnostics for required PATCH body properties", async () => { + await tester + .expect( + ` + ${patchOperation("WidgetPatchBody")} + + model WidgetPatchBody { + displayName: string; + } + `, + ) + .toEmitDiagnostics({ + code: ruleCode, + message: "Properties of a PATCH request body must not be required, property:displayName.", + }); + }); + + it("emits diagnostics for required properties in nullable top-level PATCH bodies", async () => { + await tester + .expect( + ` + ${patchOperation("WidgetPatchBody | null")} + + model WidgetPatchBody { + displayName: string; + } + `, + ) + .toEmitDiagnostics({ + code: ruleCode, + message: "Properties of a PATCH request body must not be required, property:displayName.", + }); + }); + + it("emits diagnostics for required properties in nullable nested PATCH models", async () => { + await tester + .expect( + ` + ${patchOperation("WidgetPatchBody")} + + model WidgetPatchBody { + details?: WidgetPatchDetails | null; + } + + model WidgetPatchDetails { + displayName: string; + } + `, + ) + .toEmitDiagnostics({ + code: ruleCode, + message: + "Properties of a PATCH request body must not be required, property:details.displayName.", + }); + }); + + it("emits diagnostics for discriminator properties that Autorest requires", async () => { + await tester + .expect( + ` + ${patchOperation("WidgetPatchBody")} + + model WidgetPatchBody { + optional?: OptionalDiscriminator; + synthesized?: SynthesizedDiscriminator; + derived?: DerivedDiscriminator; + } + + @discriminator("kind") + model OptionalDiscriminator { + kind?: string; + } + + @discriminator("kind") + model SynthesizedDiscriminator {} + + @discriminator("kind") + model BaseSynthesizedDiscriminator {} + + model DerivedDiscriminator extends BaseSynthesizedDiscriminator { + kind: "derived"; + } + `, + ) + .toEmitDiagnostics([ + { + code: ruleCode, + message: + "Properties of a PATCH request body must not be required, property:optional.kind.", + }, + { + code: ruleCode, + message: + "Properties of a PATCH request body must not be required, property:synthesized.kind.", + }, + { + code: ruleCode, + message: + "Properties of a PATCH request body must not be required, property:derived.kind.", + }, + ]); + }); + + it("emits diagnostics for PATCH body properties with defaults", async () => { + await tester + .expect( + ` + ${patchOperation("WidgetPatchBody")} + + model WidgetPatchBody { + enabled?: boolean = false; + count?: int32 = 0; + label?: string = ""; + mode?: string = "active"; + } + `, + ) + .toEmitDiagnostics([ + { + code: ruleCode, + message: + "Properties of a PATCH request body must not have default value, property:enabled.", + }, + { + code: ruleCode, + message: + "Properties of a PATCH request body must not have default value, property:count.", + }, + { + code: ruleCode, + message: + "Properties of a PATCH request body must not have default value, property:label.", + }, + { + code: ruleCode, + message: "Properties of a PATCH request body must not have default value, property:mode.", + }, + ]); + }); + + it("emits diagnostics for PATCH body properties that are only visible on create", async () => { + await tester + .expect( + ` + ${patchOperation("WidgetPatchBody")} + + model WidgetPatchBody { + @visibility(Lifecycle.Create) + createdBy?: string; + } + `, + ) + .toEmitDiagnostics({ + code: ruleCode, + message: + 'Properties of a PATCH request body must not be x-ms-mutability: ["create"], property:createdBy.', + }); + }); + + it("emits diagnostics for authored identity properties encoded away from top-level identity", async () => { + await tester + .expect( + ` + ${patchOperation("WidgetPatchBody")} + + model WidgetPatchBody { + @encodedName("application/json", "notIdentity") + identity: string; + } + `, + ) + .toEmitDiagnostics({ + code: ruleCode, + message: "Properties of a PATCH request body must not be required, property:notIdentity.", + }); + }); + + it("reports library property diagnostics on project properties", async () => { + await tester + .expect( + ` + ${patchOperation("WidgetPatchBody")} + + model Item {} + + model WidgetPatchBody { + /*core*/page?: Azure.Core.Page; + /*arm*/managedIdentity?: Azure.ResourceManager.CommonTypes.ManagedServiceIdentity; + identity: Azure.ResourceManager.CommonTypes.ManagedServiceIdentity; + } + `, + ) + .toEmitDiagnostics((x) => [ + { + code: ruleCode, + message: "Properties of a PATCH request body must not be required, property:page.value.", + pos: x.pos.core.pos, + }, + { + code: ruleCode, + message: + "Properties of a PATCH request body must not be required, property:managedIdentity.type.", + pos: x.pos.arm.pos, + }, + ]); + }); +}); + +describe("valid cases", () => { + it("allows optional PATCH body properties", async () => { + await tester + .expect( + ` + ${patchOperation("WidgetPatchBody")} + + model WidgetPatchBody { + displayName?: string; + } + `, + ) + .toBeValid(); + }); + + it("allows unsupported unions with multiple model variants", async () => { + await tester + .expect( + ` + ${patchOperation("FirstPatchBody | SecondPatchBody")} + + model FirstPatchBody { + first: string; + } + + model SecondPatchBody { + second: string; + } + `, + ) + .toBeValid(); + }); + + it("allows required PATCH body properties omitted because their type is never", async () => { + await tester + .expect( + ` + ${patchOperation("WidgetPatchBody")} + + model WidgetPatchBody { + omitted: never; + } + `, + ) + .toBeValid(); + }); + + it("allows top-level identity PATCH body properties", async () => { + await tester + .expect( + ` + ${patchOperation("WidgetPatchBody")} + + model WidgetPatchBody { + identity: string; + } + `, + ) + .toBeValid(); + }); + + it("allows top-level identity discriminator properties synthesized by Autorest", async () => { + await tester + .expect( + ` + ${patchOperation("WidgetPatchBody")} + + @discriminator("identity") + model WidgetPatchBody {} + `, + ) + .toBeValid(); + }); + + it("allows PATCH body properties encoded as top-level identity", async () => { + await tester + .expect( + ` + ${patchOperation("WidgetPatchBody")} + + model WidgetPatchBody { + @encodedName("application/json", "identity") + tenantIdentity: string; + } + `, + ) + .toBeValid(); + }); + + it("allows required and create-only source properties removed from the emitted PATCH schema", async () => { + await tester + .expect( + ` + @armProviderNamespace + namespace Microsoft.TestService; + + model WidgetProperties { + displayName: string; + @visibility(Lifecycle.Create) + createdBy: string; + } + + #suppress "@typespec/http/deprecated-implicit-optionality" "Test legacy PATCH transform." + @route("/widgets/{name}") + @patch(#{ implicitOptionality: true }) + op update(@path name: string, @body body: WidgetProperties): void; + `, + ) + .toBeValid(); + }); +}); diff --git a/packages/typespec-azure-rulesets/src/rulesets/resource-manager.ts b/packages/typespec-azure-rulesets/src/rulesets/resource-manager.ts index 318becda2f..cd7b75ddca 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/missing-x-ms-identifiers": true, "@azure-tools/typespec-azure-resource-manager/no-response-body": true, "@azure-tools/typespec-azure-resource-manager/missing-operations-endpoint": true, + "@azure-tools/typespec-azure-resource-manager/no-unsafe-patch-body-properties": false, "@azure-tools/typespec-azure-resource-manager/patch-envelope": true, "@azure-tools/typespec-azure-resource-manager/arm-resource-patch": true, "@azure-tools/typespec-azure-resource-manager/resource-name": true, 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 6dfa1d5aee..5e0c1be249 100644 --- a/website/src/content/docs/docs/howtos/ARM/arm-rules.md +++ b/website/src/content/docs/docs/howtos/ARM/arm-rules.md @@ -129,6 +129,7 @@ The tables below provide guidance to rule authors and ARM reviewers on how to ev | [`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-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-unsafe-patch-body-properties`](https://azure.github.io/typespec-azure/docs/libraries/azure-resource-manager/rules/no-unsafe-patch-body-properties/) | [`PatchBodyParametersSchema`](https://github.com/Azure/azure-openapi-validator/blob/main/docs/patch-body-parameters-schema.md) (RPC-Patch-V1-10) | **API.** Required, default-valued, or create-only PATCH properties prevent independent partial updates. | | [`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. | | [`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. | 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 8c63e5f5c0..f21642d116 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 @@ -58,6 +58,7 @@ Available ruleSets: | [`@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. | | [`@azure-tools/typespec-azure-resource-manager/missing-operations-endpoint`](../rules/missing-operations-endpoint.md) | Check for missing Operations interface. | +| [`@azure-tools/typespec-azure-resource-manager/no-unsafe-patch-body-properties`](../rules/no-unsafe-patch-body-properties.md) | ARM PATCH body properties must not be required, have defaults, or be create-only. | | [`@azure-tools/typespec-azure-resource-manager/patch-envelope`](../rules/patch-envelope.md) | Patch envelope properties should match the resource properties. | | [`@azure-tools/typespec-azure-resource-manager/arm-resource-patch`](../rules/arm-resource-patch.md) | Validate ARM PATCH operations. | | [`@azure-tools/typespec-azure-resource-manager/resource-name`](../rules/resource-name.md) | Check the resource name. |