Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .chronus/changes/arm-use-update-for-patch.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
changeKind: feature
packages:
- "@azure-tools/typespec-azure-resource-manager"
---

Add the `use-update-for-patch` lint rule to require authored ARM PATCH operation names to start with `update`.
7 changes: 7 additions & 0 deletions .chronus/changes/register-use-update-for-patch.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
changeKind: internal
packages:
- "@azure-tools/typespec-azure-rulesets"
---

Register the ARM `use-update-for-patch` lint rule as disabled in the resource manager ruleset.
1 change: 1 addition & 0 deletions packages/typespec-azure-resource-manager/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ Available ruleSets:
| [`@azure-tools/typespec-azure-resource-manager/retry-after`](https://azure.github.io/typespec-azure/docs/libraries/azure-resource-manager/rules/retry-after) | Check if retry-after header appears in response body. |
| [`@azure-tools/typespec-azure-resource-manager/unsupported-type`](https://azure.github.io/typespec-azure/docs/libraries/azure-resource-manager/rules/unsupported-type) | Check for unsupported ARM types. |
| [`@azure-tools/typespec-azure-resource-manager/use-model-request-body`](https://azure.github.io/typespec-azure/docs/libraries/azure-resource-manager/rules/use-model-request-body) | Request bodies must use plain models. |
| [`@azure-tools/typespec-azure-resource-manager/use-update-for-patch`](https://azure.github.io/typespec-azure/docs/libraries/azure-resource-manager/rules/use-update-for-patch) | ARM PATCH operation names should start with 'update'. |
| [`@azure-tools/typespec-azure-resource-manager/secret-prop`](https://azure.github.io/typespec-azure/docs/libraries/azure-resource-manager/rules/secret-prop) | RPC-v1-13: Check that property with names indicating sensitive information(e.g. contains auth, password, token, secret, etc.) are marked with @secret decorator. |
| [`@azure-tools/typespec-azure-resource-manager/no-empty-model`](https://azure.github.io/typespec-azure/docs/libraries/azure-resource-manager/rules/no-empty-model) | ARM Properties with type:object that don't reference a model definition are not allowed. ARM doesn't allow generic type definitions as this leads to bad customer experience. |
| [`@azure-tools/typespec-azure-resource-manager/no-reserved-resource-property`](https://azure.github.io/typespec-azure/docs/libraries/azure-resource-manager/rules/no-reserved-resource-property) | Reserved property names (for example 'billingData') must not be present in a resource's property bag. The property name is matched case-insensitively. |
Expand Down
2 changes: 2 additions & 0 deletions packages/typespec-azure-resource-manager/src/linter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ import { useInterfaceRule } from "./rules/use-interface.js";
import { useModelRequestBodyRule } from "./rules/use-model-request-body.js";
import { useOperationDecoratorRule } from "./rules/use-operation-decorator.js";
import { useRelationshipRequiredPropertiesRule } from "./rules/use-relationship-required-properties.js";
import { useUpdateForPatchRule } from "./rules/use-update-for-patch.js";
import { versionProgressionRule } from "./rules/version-progression.js";

const rules = [
Expand Down Expand Up @@ -102,6 +103,7 @@ const rules = [
retryAfterRule,
unsupportedTypeRule,
useModelRequestBodyRule,
useUpdateForPatchRule,
secretProprule,
noEmptyModel,
noReservedResourcePropertyRule,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
ARM HTTP PATCH operation names should start with `update`, ignoring case. Use names such as
`update`, `updateTags`, or `updateWidget` instead of `patch`, `modify`, or `create`.
This check applies to concrete HTTP service endpoints regardless of their response status codes.

Consistent partial-update names make ARM APIs easier to discover and give generated SDKs a
predictable operation naming convention. The check uses the authored operation name, not an SDK
name override or the containing interface name.

## Incorrect

```tsp
@armProviderNamespace
@service
@armCommonTypesVersion(CommonTypes.Versions.v5)
namespace Microsoft.Contoso;

model Widget is TrackedResource<WidgetProperties> {
@key("widgetName")
@segment("widgets")
name: string;
}

model WidgetProperties {
provisioningState?: ResourceProvisioningState;
}

@armResourceOperations
interface Widgets {
modify is ArmResourcePatchSync<Widget, Properties = WidgetProperties>;
}
```

## Correct

```tsp
@armProviderNamespace
@service
@armCommonTypesVersion(CommonTypes.Versions.v5)
namespace Microsoft.Contoso;

model Widget is TrackedResource<WidgetProperties> {
@key("widgetName")
@segment("widgets")
name: string;
}

model WidgetProperties {
provisioningState?: ResourceProvisioningState;
}

@armResourceOperations
interface Widgets {
update is ArmResourcePatchSync<Widget, Properties = WidgetProperties>;
}
```

The examples assume the usual ARM library imports and `using` declarations.

## Impact

**API, SDK.** Inconsistent partial-update operation names make APIs harder to discover. Renaming
an already shipped operation may break existing SDK callers.

## Suppression

Suppress this warning when compatibility requires preserving a shipped operation name. Prefer
an `update` prefix for new operations.

```tsp
@armResourceOperations
interface Widgets {
#suppress "@azure-tools/typespec-azure-resource-manager/use-update-for-patch" "Preserve the shipped SDK operation name."
modify is ArmResourcePatchSync<Widget, Properties = WidgetProperties>;
}
```

## LintDiff Equivalent

This rule corresponds to
[`PatchInOperationName`](https://github.com/Azure/azure-openapi-validator/blob/main/docs/patch-in-operation-name.md).
It enforces authored ARM endpoint names rather than reproducing emitted-operation-ID exemptions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { createRule, fileRef, paramMessage } from "@typespec/compiler";
import { getAllHttpServices } from "@typespec/http";

export const useUpdateForPatchRule = createRule({
name: "use-update-for-patch",
docs: fileRef.fromPackageRoot("src/rules/use-update-for-patch.md"),
description: "ARM PATCH operation names should start with 'update'.",
url: "https://azure.github.io/typespec-azure/docs/libraries/azure-resource-manager/rules/use-update-for-patch",
severity: "warning",
messages: {
default: paramMessage`ARM PATCH operation '${"operationName"}' should start with 'update'.`,
},
create(context) {
return {
root: () => {
const [services] = getAllHttpServices(context.program);
for (const service of services) {
for (const { operation, verb } of service.operations) {
if (verb !== "patch" || operation.name.toLowerCase().startsWith("update")) {
continue;
}

context.reportDiagnostic({
target: operation,
format: { operationName: operation.name },
});
}
}
},
};
},
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import { Tester } from "#test/tester.js";
import { createLinterRuleTester, type LinterRuleTester } from "@typespec/compiler/testing";
import { readFile } from "node:fs/promises";
import { beforeEach, it } from "vitest";
import { useUpdateForPatchRule } from "../../src/rules/use-update-for-patch.js";

let tester: LinterRuleTester;

beforeEach(async () => {
tester = createLinterRuleTester(
await Tester.createInstance(),
useUpdateForPatchRule,
"@azure-tools/typespec-azure-resource-manager",
);
});

const header = "@service namespace Microsoft.Contoso;";

function diagnostic(name: string) {
return {
code: "@azure-tools/typespec-azure-resource-manager/use-update-for-patch",
message: `ARM PATCH operation '${name}' should start with 'update'.`,
};
}

it.each(["patch", "modify", "create", "patch_Update"])("rejects PATCH name %s", async (name) => {
await tester
.expect(`${header} @patch @route("/item") op ${name}(): string;`)
.toEmitDiagnostics([diagnostic(name)]);
});

it.each(["update", "updateWidget", "UpdateTags"])("accepts update prefix %s", async (name) => {
await tester.expect(`${header} @patch @route("/item") op ${name}(): string;`).toBeValid();
});

it.each([200, 201, 202, 204])("checks PATCH names for response %s", async (status) => {
await tester
.expect(`${header} @patch @route("/item") op modify(): { @statusCode code: ${status}; };`)
.toEmitDiagnostics([diagnostic("modify")]);
});

it("ignores non-PATCH verbs", async () => {
await tester.expect(`${header} @get @route("/item") op modify(): string;`).toBeValid();
});

it("does not let an interface name mask the operation prefix", async () => {
await tester
.expect(`${header} @route("/item") interface UpdateWidgets { @patch op modify(): string; }`)
.toEmitDiagnostics([diagnostic("modify")]);
});

it("reports distinct offending operation names without a provider decorator", async () => {
await tester
.expect(
`
${header}
@patch @route("/first") op modifyFirst(): string;
@patch @route("/second") op modifySecond(): string;
`,
)
.toEmitDiagnostics([diagnostic("modifyFirst"), diagnostic("modifySecond")]);
});

it("validates the exact incorrect documentation example", async () => {
const source = await readFile(
new URL("../../src/rules/use-update-for-patch.md", import.meta.url),
"utf8",
);
const snippet = source
.replace(/\r\n/g, "\n")
.split("## Incorrect")[1]
.match(/```tsp\n([\s\S]*?)```/)![1];
await tester.expect(snippet).toEmitDiagnostics([diagnostic("modify")]);
});

it("validates the exact correct documentation example", async () => {
const source = await readFile(
new URL("../../src/rules/use-update-for-patch.md", import.meta.url),
"utf8",
);
const snippet = source
.replace(/\r\n/g, "\n")
.split("## Correct")[1]
.match(/```tsp\n([\s\S]*?)```/)![1];
await tester.expect(snippet).toBeValid();
});
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@ export default {
"@azure-tools/typespec-azure-resource-manager/unsupported-type": true,
"@azure-tools/typespec-azure-resource-manager/no-query-in-point-op": false,
"@azure-tools/typespec-azure-resource-manager/use-model-request-body": false,
"@azure-tools/typespec-azure-resource-manager/use-update-for-patch": false,

// TCGC rules
"@azure-tools/typespec-client-generator-core/require-client-suffix": true,
Expand Down
1 change: 1 addition & 0 deletions website/src/content/docs/docs/howtos/ARM/arm-rules.md
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,7 @@ The tables below provide guidance to rule authors and ARM reviewers on how to ev
| [`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-update-for-patch`](https://azure.github.io/typespec-azure/docs/libraries/azure-resource-manager/rules/use-update-for-patch/) | [`PatchInOperationName`](https://github.com/Azure/azure-openapi-validator/blob/main/docs/patch-in-operation-name.md) | **API, SDK.** Inconsistent authored PATCH names make partial-update operations harder to discover; renaming shipped operations may break SDK callers. |
| [`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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ Available ruleSets:
| [`@azure-tools/typespec-azure-resource-manager/retry-after`](../rules/retry-after.md) | Check if retry-after header appears in response body. |
| [`@azure-tools/typespec-azure-resource-manager/unsupported-type`](../rules/unsupported-type.md) | Check for unsupported ARM types. |
| [`@azure-tools/typespec-azure-resource-manager/use-model-request-body`](../rules/use-model-request-body.md) | Request bodies must use plain models. |
| [`@azure-tools/typespec-azure-resource-manager/use-update-for-patch`](../rules/use-update-for-patch.md) | ARM PATCH operation names should start with 'update'. |
| [`@azure-tools/typespec-azure-resource-manager/secret-prop`](../rules/secret-prop.md) | RPC-v1-13: Check that property with names indicating sensitive information(e.g. contains auth, password, token, secret, etc.) are marked with @secret decorator. |
| [`@azure-tools/typespec-azure-resource-manager/no-empty-model`](../rules/no-empty-model.md) | ARM Properties with type:object that don't reference a model definition are not allowed. ARM doesn't allow generic type definitions as this leads to bad customer experience. |
| [`@azure-tools/typespec-azure-resource-manager/no-reserved-resource-property`](../rules/no-reserved-resource-property.md) | Reserved property names (for example 'billingData') must not be present in a resource's property bag. The property name is matched case-insensitively. |
Expand Down
Loading