Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .chronus/changes/arm-use-even-segmented-put-path.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-even-segmented-put-path` lint rule to check that ARM PUT paths end in resource type/name pairs.
Comment thread
msyyc marked this conversation as resolved.
Outdated
7 changes: 7 additions & 0 deletions .chronus/changes/register-use-even-segmented-put-path.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-even-segmented-put-path` 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 @@ -81,6 +81,7 @@ Available ruleSets:
| [`@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. |
| [`@azure-tools/typespec-azure-resource-manager/no-tenant-level-apis`](https://azure.github.io/typespec-azure/docs/libraries/azure-resource-manager/rules/no-tenant-level-apis) | ARM PUT operations whose resolved paths begin with '/providers' are not allowed, except paths ending in '/operations'. |
| [`@azure-tools/typespec-azure-resource-manager/no-query-in-point-op`](https://azure.github.io/typespec-azure/docs/libraries/azure-resource-manager/rules/no-query-in-point-op) | Point operations must not declare query parameters beyond api-version. |
| [`@azure-tools/typespec-azure-resource-manager/use-even-segmented-put-path`](https://azure.github.io/typespec-azure/docs/libraries/azure-resource-manager/rules/use-even-segmented-put-path) | ARM PUT paths must end in resource type and resource name pairs. |

## Decorators

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 @@ -47,6 +47,7 @@ import { secretProprule } from "./rules/secret-prop.js";
import { unsupportedTypeRule } from "./rules/unsupported-type.js";
import { useApiVersionRule } from "./rules/use-api-version.js";
import { useApplicationJsonContentTypeRule } from "./rules/use-application-json-content-type.js";
import { useEvenSegmentedPutPathRule } from "./rules/use-even-segmented-put-path.js";
import { useInterfaceRule } from "./rules/use-interface.js";
import { useModelRequestBodyRule } from "./rules/use-model-request-body.js";
import { useOperationDecoratorRule } from "./rules/use-operation-decorator.js";
Expand Down Expand Up @@ -107,6 +108,7 @@ const rules = [
noReservedResourcePropertyRule,
noTenantLevelApisRule,
noQueryInPointOpRule,
useEvenSegmentedPutPathRule,
];

export const $linter = defineLinter({
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
ARM PUT operations create or replace a resource instance. Their HTTP paths must
end in resource type/name pairs after the provider namespace, such as
`/providers/Microsoft.Contoso/widgets/{widgetName}`. A singleton may use the
literal name `default`, and nested resources add further type/name pairs.

A provider root, collection path, or unmatched suffix such as
`/widgets/{widgetName}/{configurationName}` is not a resource instance path for PUT.
Prefer standard ARM resource operation templates, which construct resource paths
for you. This rule also checks custom PUT operations when explicitly enabled.

The rule checks resolved HTTP paths on project operations, including ordinary
and nested namespaces without a provider decorator. It excludes imported library
declarations, template instances, and operations in uninstantiated template
interfaces. It does not filter operations by SDK language scope.

## Impact

- **Area:** API, SDK

Using PUT for a collection or action endpoint obscures the identity of the
resource being created or replaced and gives generated SDKs an inconsistent
resource-management contract.

## ❌ Incorrect

```tsp
@armProviderNamespace
namespace Microsoft.Contoso;

model Widget is TrackedResource<{}> {
...ResourceNameParameter<Widget>;
}

@armResourceOperations
interface Widgets {
createOrUpdate is ArmResourceCreateOrReplaceSync<
Widget,
Parameters = {
@path configurationName: string;
}
>;
}
```

## ✅ Correct

Use the standard ARM create-or-replace template without the additional path
parameter, so the PUT targets the resource instance:

```tsp
@armProviderNamespace
namespace Microsoft.Contoso;

model Widget is TrackedResource<{}> {
...ResourceNameParameter<Widget>;
}

@armResourceOperations
interface Widgets {
createOrUpdate is ArmResourceCreateOrReplaceSync<Widget>;
}
```

## Suppression

Suppress this rule only for a reviewed legacy ARM contract whose PUT path cannot
be changed, or a client-only operation that is not part of the service's ARM
surface. Document the reason on the operation. For new APIs, use a resource
instance path rather than suppressing a collection or action-like PUT.

## LintDiff Equivalent

This rule corresponds to
[EvenSegmentedPathForPutOperation](https://github.com/Azure/azure-openapi-validator/blob/main/docs/even-segmented-path-for-put-operation.md).
It preserves the validator's provider/resource suffix pattern rather than
counting every segment in the entire path. SDK-scoped TypeSpec operations that
are absent from retained Swagger can still receive a diagnostic.
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import {
createRule,
fileRef,
getLocationContext,
isTemplateInstance,
paramMessage,
} from "@typespec/compiler";
import { getHttpOperation } from "@typespec/http";
import { isTemplatedInterfaceOperation } from "./utils.js";

const evenSegmentedArmPutPathPattern = /.*\/providers\/\w+\.\w+(\/\w+\/(default|\{\w+\}))+$/;

export const useEvenSegmentedPutPathRule = createRule({
name: "use-even-segmented-put-path",
description: "ARM PUT paths must end in resource type and resource name pairs.",
severity: "warning",
url: "https://azure.github.io/typespec-azure/docs/libraries/azure-resource-manager/rules/use-even-segmented-put-path",
docs: fileRef.fromPackageRoot("src/rules/use-even-segmented-put-path.md"),
messages: {
default: paramMessage`ARM PUT path '${"path"}' must end in repeated /{resourceType}/{resourceName} or /{resourceType}/default pairs after the provider namespace.`,
},
create(context) {
return {
operation: (operation) => {
if (
isTemplateInstance(operation) ||
isTemplatedInterfaceOperation(operation) ||
getLocationContext(context.program, operation).type !== "project"
) {
return;
}

const [httpOperation] = getHttpOperation(context.program, operation);
if (
httpOperation.verb !== "put" ||
evenSegmentedArmPutPathPattern.test(httpOperation.path)
) {
return;
}

context.reportDiagnostic({
target: operation,
format: { path: httpOperation.path },
});
},
};
},
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
import { Tester } from "#test/tester.js";
import { type LinterRuleTester, createLinterRuleTester } from "@typespec/compiler/testing";
import { readFileSync } from "node:fs";
import { beforeEach, it } from "vitest";
import { useEvenSegmentedPutPathRule } from "../../src/rules/use-even-segmented-put-path.js";

let tester: LinterRuleTester;

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

function diagnostic(path: string) {
return {
code: "@azure-tools/typespec-azure-resource-manager/use-even-segmented-put-path",
severity: "warning" as const,
message: `ARM PUT path '${path}' must end in repeated /{resourceType}/{resourceName} or /{resourceType}/default pairs after the provider namespace.`,
};
}

it("rejects a provider-root PUT and targets the operation", async () => {
await tester
.expect(
`
/*target*/@route("/providers/Microsoft.Contoso")
@put op create(): void;
`,
)
.toEmitDiagnostics((x) => ({
...diagnostic("/providers/Microsoft.Contoso"),
pos: x.pos.target.pos,
}));
});

it("rejects a collection-level PUT", async () => {
await tester
.expect(
`
@route("/providers/Microsoft.Contoso/widgets")
@put op create(): void;
`,
)
.toEmitDiagnostics(diagnostic("/providers/Microsoft.Contoso/widgets"));
});

it("rejects an odd suffix after a resource instance", async () => {
await tester
.expect(
`
@route("/providers/Microsoft.Contoso/widgets/{widgetName}/config")
@put op create(@path widgetName: string): void;
`,
)
.toEmitDiagnostics(diagnostic("/providers/Microsoft.Contoso/widgets/{widgetName}/config"));
});

it("accepts standard ARM create-or-update templates", async () => {
await tester
.expect(
`
@armProviderNamespace
namespace Microsoft.Contoso;
model Widget is TrackedResource<{}> {
...ResourceNameParameter<Widget>;
}
@armResourceOperations
interface Widgets {
createOrUpdate is ArmResourceCreateOrReplaceSync<Widget>;
}
`,
)
.toBeValid();
});

it("validates the published ARM template examples", async () => {
Comment thread
msyyc marked this conversation as resolved.
Outdated
const doc = readFileSync(
new URL("../../src/rules/use-even-segmented-put-path.md", import.meta.url),
"utf8",
);
const examples = [...doc.matchAll(/```tsp\r?\n([\s\S]*?)\r?\n```/g)].map((match) => match[1]);
if (examples.length !== 2) {
throw new Error(`Expected exactly two published TypeSpec examples, got ${examples.length}`);
}

await tester.expect(examples[0]).toEmitDiagnostics((result) => {
const main = [...result.program.sourceFiles.values()].find((file) =>
file.file.text.includes("createOrUpdate is ArmResourceCreateOrReplaceSync"),
);
const pos = main?.file.text.indexOf("createOrUpdate is ArmResourceCreateOrReplaceSync");
if (!main || pos === undefined || pos < 0) {
throw new Error("The published createOrUpdate operation was not compiled");
}
return {
...diagnostic(
"/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.Contoso/widgets/{widgetName}/{configurationName}",
),
file: main.file.path,
pos,
};
});
await tester.expect(examples[1]).toBeValid();
});

it("accepts a singleton default resource", async () => {
await tester
.expect(
`
@route("/providers/Microsoft.Contoso/widgets/default")
@put op create(): void;
`,
)
.toBeValid();
});

it("accepts nested resource type and name pairs", async () => {
await tester
.expect(
`
@route("/providers/Microsoft.Contoso/widgets/{widgetName}/configs/{configName}")
@put op create(@path widgetName: string, @path configName: string): void;
`,
)
.toBeValid();
});

it("accepts a scope-prefixed resource path", async () => {
await tester
.expect(
`
@route("/{scope}/providers/Microsoft.Contoso/widgets/{widgetName}")
@put op create(@path scope: string, @path widgetName: string): void;
`,
)
.toBeValid();
});

it("ignores non-PUT operations", async () => {
await tester
.expect(
`
@route("/providers/Microsoft.Contoso/widgets")
@post op create(): void;
@route("/providers/Microsoft.Contoso")
@get op read(): void;
`,
)
.toBeValid();
});

it("ignores operation template instances while checking project declarations", async () => {
await tester
.expect(
`
@route("/providers/Microsoft.Template")
@put op Template<T>(@body body: T): void;
alias Instance = Template<string>;
@route("/providers/Microsoft.Contoso")
@put op create(): void;
`,
)
.toEmitDiagnostics(diagnostic("/providers/Microsoft.Contoso"));
});
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-even-segmented-put-path": 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 @@ -134,6 +134,7 @@ The tables below provide guidance to rule authors and ARM reviewers on how to ev
| [`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-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-even-segmented-put-path`](https://azure.github.io/typespec-azure/docs/libraries/azure-resource-manager/rules/use-even-segmented-put-path/) | [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. |
Expand Down
Loading
Loading