Skip to content
Merged
7 changes: 7 additions & 0 deletions .chronus/changes/arm-resource-missing-read.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 `resource-missing-read` lint rule requiring a read operation for ARM resources with create or update operations.
7 changes: 7 additions & 0 deletions .chronus/changes/register-resource-missing-read.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 `resource-missing-read` 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 @@ -73,6 +73,7 @@ Available ruleSets:
| [`@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. |
| [`@azure-tools/typespec-azure-resource-manager/resource-missing-read`](https://azure.github.io/typespec-azure/docs/libraries/azure-resource-manager/rules/resource-missing-read) | ARM resources with create or update operations must define a read operation. |
| [`@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. |
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 @@ -41,6 +41,7 @@ import { noResponseBodyRule } from "./rules/no-response-body.js";
import { noTenantLevelApisRule } from "./rules/no-tenant-level-apis.js";
import { operationsInterfaceMissingRule } from "./rules/operations-interface-missing.js";
import { patchEnvelopePropertiesRules } from "./rules/patch-envelope-properties.js";
import { resourceMissingReadRule } from "./rules/resource-missing-read.js";
import { resourceNameRule } from "./rules/resource-name.js";
import { retryAfterRule } from "./rules/retry-after.js";
import { secretProprule } from "./rules/secret-prop.js";
Expand Down Expand Up @@ -99,6 +100,7 @@ const rules = [
patchEnvelopePropertiesRules,
patchOperationsRule,
resourceNameRule,
resourceMissingReadRule,
retryAfterRule,
unsupportedTypeRule,
useModelRequestBodyRule,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
An ARM resource with a create or update lifecycle operation must also provide a read operation.
Clients need to retrieve the resource state after creating or updating it.

Define `ArmResourceRead<Resource>` alongside the resource's create/update operations. A collection
list does not replace a read of an individual resource. Delete-only resources are outside this
rule's scope.

## Impact

- **Area:** API

Without a read operation, clients cannot retrieve the state of a resource they can create or update.
The rule reports one warning per resource, on the concrete interface declaring its registered
create/update operation. Having both create and update operations does not produce two warnings.
When writes are split across interfaces, the create operation's interface takes priority over
the update operation's interface. A separate delete interface is not the diagnostic target.

## ❌ Incorrect

For an existing `Widget` resource with `WidgetProperties`, this interface omits the read operation:

```typespec
@armResourceOperations
interface Widgets {
createOrUpdate is ArmResourceCreateOrReplaceSync<Widget>;
update is ArmResourcePatchSync<Widget, WidgetProperties>;
}
```

## ✅ Correct

Add a read using the standard ARM resource operation template:

```typespec
@armResourceOperations
interface Widgets {
createOrUpdate is ArmResourceCreateOrReplaceSync<Widget>;
update is ArmResourcePatchSync<Widget, WidgetProperties>;
get is ArmResourceRead<Widget>;
}
```

These examples assume a model such as
`model Widget is TrackedResource<WidgetProperties> { ...ResourceNameParameter<Widget>; }`,
with a named properties model such as `model WidgetProperties { label?: string; }`.

## Suppression

Prefer adding the standard read operation. Suppress this rule only when an ARM reviewer approves
a resource design that intentionally cannot be read; include the approval reason in the
`#suppress` directive on the resource operation interface.
Place suppression on the concrete create interface, or on the update interface when there is no
create operation. Suppression on a separate delete interface does not suppress this warning.

## LintDiff Equivalent

This rule corresponds to
[AllResourcesMustHaveGetOperation](https://github.com/Azure/azure-openapi-validator/blob/main/docs/all-resources-must-have-get-operation.md).
It checks registered native ARM lifecycle roles, not emitted response references. Legacy HTTP GET
operations without ARM read metadata do not satisfy this check; registered reads with customized
responses do. Full executable Swagger equivalence is not claimed. The discriminator-ancestor
exemption is retained, but a clean supported polymorphic ARM fixture has not been established.
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import {
createRule,
fileRef,
getDiscriminator,
paramMessage,
type Interface,
type Model,
type Program,
} from "@typespec/compiler";
import type { ArmResourceOperation } from "../operations.js";
import { getArmResources, type ArmResourceDetails } from "../resource.js";
import { getInterface } from "./utils.js";

export const resourceMissingReadRule = createRule({
name: "resource-missing-read",
description: "ARM resources with create or update operations must define a read operation.",
docs: fileRef.fromPackageRoot("src/rules/resource-missing-read.md"),
url: "https://azure.github.io/typespec-azure/docs/libraries/azure-resource-manager/rules/resource-missing-read",
severity: "warning",
messages: {
default: paramMessage`Resource '${"name"}' must have a get/read operation.`,
},
create(context) {
return {
root: (program) => {
for (const resource of getArmResources(program)) {
if (
!requiresRead(resource) ||
resource.operations.lifecycle.read !== undefined ||
hasDiscriminatorAncestor(program, resource.typespecType)
) {
continue;
}

context.reportDiagnostic({
target: getDiagnosticTarget(resource),
format: { name: resource.name },
});
}
},
};
},
});

function requiresRead(resource: ArmResourceDetails): boolean {
return (
resource.operations.lifecycle.createOrUpdate !== undefined ||
resource.operations.lifecycle.update !== undefined
);
}

function hasDiscriminatorAncestor(program: Program, model: Model): boolean {
for (let current = model.baseModel; current !== undefined; current = current.baseModel) {
if (getDiscriminator(program, current) !== undefined) {
return true;
}
}
return false;
}

function getDiagnosticTarget(resource: ArmResourceDetails): Interface | Model {
return (
resource.operations.lifecycle.createOrUpdate?.operation.interface ??
resource.operations.lifecycle.update?.operation.interface ??
getInterface(resource) ??
Comment thread
msyyc marked this conversation as resolved.
getOperationInterface(Object.values(resource.operations.lists)) ??
getOperationInterface(Object.values(resource.operations.actions)) ??
resource.typespecType
);
}

function getOperationInterface(
operations: Array<ArmResourceOperation | undefined>,
): Interface | undefined {
return operations.find((operation) => operation?.operation.interface !== undefined)?.operation
.interface;
}
Comment thread
msyyc marked this conversation as resolved.
Outdated
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { createLinterRuleTester } from "@typespec/compiler/testing";
import { readFile } from "node:fs/promises";
import { it } from "vitest";
import { resourceMissingReadRule } from "../../src/rules/resource-missing-read.js";
import { Tester } from "../tester.js";

it.each([
["❌ Incorrect", true],
["✅ Correct", false],
] as const)("compiles the published %s resource example", async (heading, violates) => {
const documentation = await readFile(
Comment thread
msyyc marked this conversation as resolved.
Outdated
new URL("../../src/rules/resource-missing-read.md", import.meta.url),
"utf8",
);
const section = documentation
.split(/^## /m)
.find((text) => text.split(/\r?\n/, 1)[0] === heading);
const snippet = section?.match(/```typespec\r?\n([\s\S]*?)```/)?.[1];
if (!snippet) throw new Error(`Missing resource-missing-read example in ${heading}.`);
const tester = createLinterRuleTester(
await Tester.createInstance(),
resourceMissingReadRule,
"@azure-tools/typespec-azure-resource-manager",
);
const fixture = tester.expect(`
@armProviderNamespace @service namespace Microsoft.Contoso;
model WidgetProperties { label?: string; }
model Widget is TrackedResource<WidgetProperties> {
...ResourceNameParameter<Widget>;
}
${snippet}
`);
if (violates) {
await fixture.toEmitDiagnostics({
code: "@azure-tools/typespec-azure-resource-manager/resource-missing-read",
message: "Resource 'Widget' must have a get/read operation.",
});
} else {
await fixture.toBeValid();
}
});
Loading
Loading