diff --git a/.chronus/changes/arm-no-repeated-path-info.md b/.chronus/changes/arm-no-repeated-path-info.md new file mode 100644 index 0000000000..31fc7797e7 --- /dev/null +++ b/.chronus/changes/arm-no-repeated-path-info.md @@ -0,0 +1,9 @@ +--- +changeKind: feature +packages: + - "@azure-tools/typespec-azure-resource-manager" +--- + +Add the `no-repeated-path-info` lint rule to warn when ARM PUT resource properties repeat path or query parameter names, honoring JSON-encoded property names and including concrete operations inherited from generic interfaces. + +This complements `arm-resource-duplicate-property`, which checks duplication of resource envelope property names rather than URI parameter names. diff --git a/.chronus/changes/register-no-repeated-path-info-disabled.md b/.chronus/changes/register-no-repeated-path-info-disabled.md new file mode 100644 index 0000000000..79517cf85e --- /dev/null +++ b/.chronus/changes/register-no-repeated-path-info-disabled.md @@ -0,0 +1,7 @@ +--- +changeKind: internal +packages: + - "@azure-tools/typespec-azure-rulesets" +--- + +Register the ARM `no-repeated-path-info` 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 4d28d8b253..4e481ad091 100644 --- a/packages/typespec-azure-resource-manager/README.md +++ b/packages/typespec-azure-resource-manager/README.md @@ -70,6 +70,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/no-query-in-post`](https://azure.github.io/typespec-azure/docs/libraries/azure-resource-manager/rules/no-query-in-post) | ARM POST operations must not declare query parameters other than api-version. | +| [`@azure-tools/typespec-azure-resource-manager/no-repeated-path-info`](https://azure.github.io/typespec-azure/docs/libraries/azure-resource-manager/rules/no-repeated-path-info) | ARM PUT request bodies must not repeat path or query parameters in the resource properties bag. | | [`@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/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. | diff --git a/packages/typespec-azure-resource-manager/src/linter.ts b/packages/typespec-azure-resource-manager/src/linter.ts index eb049e6dd1..35503410e2 100644 --- a/packages/typespec-azure-resource-manager/src/linter.ts +++ b/packages/typespec-azure-resource-manager/src/linter.ts @@ -35,6 +35,7 @@ import { noOverridePropsRule } from "./rules/no-override-props.js"; import { noQueryInCollectionRule } from "./rules/no-query-in-collection.js"; import { noQueryInPointOpRule } from "./rules/no-query-in-point-op.js"; import { noQueryInPostRule } from "./rules/no-query-in-post.js"; +import { noRepeatedPathInfoRule } from "./rules/no-repeated-path-info.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"; @@ -97,6 +98,7 @@ const rules = [ missingXmsIdentifiersRule, noResponseBodyRule, noQueryInPostRule, + noRepeatedPathInfoRule, operationsInterfaceMissingRule, patchEnvelopePropertiesRules, patchOperationsRule, diff --git a/packages/typespec-azure-resource-manager/src/rules/model-utils.ts b/packages/typespec-azure-resource-manager/src/rules/model-utils.ts new file mode 100644 index 0000000000..7d3a72fe26 --- /dev/null +++ b/packages/typespec-azure-resource-manager/src/rules/model-utils.ts @@ -0,0 +1,29 @@ +import { + isNullType, + resolveEncodedName, + walkPropertiesInherited, + type Model, + type ModelProperty, + type Program, + type Type, + type Union, +} from "@typespec/compiler"; + +export function getSingleNonNullMember(union: Union): Type | undefined { + let member: Type | undefined; + for (const variant of union.variants.values()) { + if (isNullType(variant.type)) continue; + if (member !== undefined) return undefined; + member = variant.type; + } + return member; +} + +export function* walkJsonProperties( + program: Program, + model: Model, +): Iterable<[string, ModelProperty]> { + for (const property of walkPropertiesInherited(model)) { + yield [resolveEncodedName(program, property, "application/json"), property]; + } +} diff --git a/packages/typespec-azure-resource-manager/src/rules/no-repeated-path-info.md b/packages/typespec-azure-resource-manager/src/rules/no-repeated-path-info.md new file mode 100644 index 0000000000..d8b4bc89c4 --- /dev/null +++ b/packages/typespec-azure-resource-manager/src/rules/no-repeated-path-info.md @@ -0,0 +1,74 @@ +ARM PUT request bodies should not repeat information already supplied in path or +query parameters. Keep resource identity in the request URI and resource-specific +data in the resource's `properties` bag, so callers do not have to supply +potentially conflicting values twice. + +This rule complements +[`arm-resource-duplicate-property`](./arm-resource-duplicate-property.md), which +compares properties-bag members with envelope property names on ARM resource +models, independently of operations. For example, an envelope `name` with +`@key("widgetName")` makes `properties.widgetName` a URI duplication only. +Conversely, `properties.identity` duplicates an envelope `identity` only under +the envelope rule when no URI parameter is named `identity`. Both rules can +report a member when its name matches both an envelope property and a URI parameter. + +## Impact + +- **Area:** API, SDK + +Repeating URI information in resource properties creates redundant SDK inputs +and ambiguity when the request URI and payload disagree. + +## ❌ Incorrect + +The standard ARM create-or-replace operation carries `widgetName` in the URI. +With `ArmResourceCreateOrReplaceSync`, repeating that key name in +`WidgetProperties` triggers this rule. These excerpts omit unchanged provider +and operation declarations. + +```tsp +model Widget is TrackedResource { + @key("widgetName") + @segment("widgets") + @path + name: string; +} + +model WidgetProperties { + widgetName?: string; + description?: string; +} +``` + +## ✅ Correct + +Keep the resource key and operation template unchanged; remove only the redundant +properties-bag member. + +```tsp +model WidgetProperties { + description?: string; +} +``` + +## Suppression + +Prefer removing the redundant properties-bag member. If a published API must +retain the duplication for compatibility and an ARM reviewer has approved the +exception, suppress the warning on that member with a specific justification: + +```tsp +model WidgetProperties { + #suppress "@azure-tools/typespec-azure-resource-manager/no-repeated-path-info" "Retained for compatibility with the approved published API." + widgetName?: string; +} +``` + +## LintDiff Equivalent + +This rule corresponds to +[RepeatedPathInfo](https://github.com/Azure/azure-openapi-validator/blob/main/docs/repeated-path-info.md) +and ARM guideline `RPC-Put-V1-05`. Like the Swagger validator, it compares JSON +property names, including JSON name overrides, with path and query parameter +names. It checks native TypeSpec operations rather than an emitted single-version +schema. diff --git a/packages/typespec-azure-resource-manager/src/rules/no-repeated-path-info.ts b/packages/typespec-azure-resource-manager/src/rules/no-repeated-path-info.ts new file mode 100644 index 0000000000..bc39021e47 --- /dev/null +++ b/packages/typespec-azure-resource-manager/src/rules/no-repeated-path-info.ts @@ -0,0 +1,82 @@ +import type { Model } from "@typespec/compiler"; +import { + createRule, + fileRef, + getProperty, + isTemplateDeclarationOrInstance, + paramMessage, +} from "@typespec/compiler"; +import { getHttpOperation } from "@typespec/http"; +import { walkJsonProperties } from "./model-utils.js"; +import { isTemplatedInterfaceOperation } from "./utils.js"; + +export const noRepeatedPathInfoRule = createRule({ + name: "no-repeated-path-info", + docs: fileRef.fromPackageRoot("src/rules/no-repeated-path-info.md"), + description: + "ARM PUT request bodies must not repeat path or query parameters in the resource properties bag.", + severity: "warning", + url: "https://azure.github.io/typespec-azure/docs/libraries/azure-resource-manager/rules/no-repeated-path-info", + messages: { + default: paramMessage`Request body property '${"name"}' repeats information already carried in the path or query.`, + }, + create(context) { + return { + operation: (operation) => { + if ( + isTemplateDeclarationOrInstance(operation) || + isTemplatedInterfaceOperation(operation) + ) { + return; + } + + const [httpOperation] = getHttpOperation(context.program, operation); + if (httpOperation.verb !== "put") { + return; + } + + const repeatedNames = new Set( + httpOperation.parameters.parameters + .filter((parameter) => parameter.type === "path" || parameter.type === "query") + .map((parameter) => parameter.name), + ); + if (repeatedNames.size === 0) { + return; + } + + const body = httpOperation.parameters.body; + if (body === undefined || body.bodyKind !== "single" || body.type.kind !== "Model") { + return; + } + + const propertiesModel = getPropertiesModel(body.type); + if (propertiesModel === undefined) { + return; + } + + const reportedNames = new Set(); + for (const [name, property] of walkJsonProperties(context.program, propertiesModel)) { + if (!repeatedNames.has(name)) { + continue; + } + if (reportedNames.has(name)) { + continue; + } + reportedNames.add(name); + + context.reportDiagnostic({ + target: property, + format: { + name, + }, + }); + } + }, + }; + }, +}); + +function getPropertiesModel(model: Model): Model | undefined { + const property = getProperty(model, "properties"); + return property?.type.kind === "Model" ? property.type : undefined; +} 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..140bc811ff --- /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("Native 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/model-utils.test.ts b/packages/typespec-azure-resource-manager/test/rules/model-utils.test.ts new file mode 100644 index 0000000000..4a65604f0b --- /dev/null +++ b/packages/typespec-azure-resource-manager/test/rules/model-utils.test.ts @@ -0,0 +1,27 @@ +import { t } from "@typespec/compiler/testing"; +import { expect, it } from "vitest"; +import { getSingleNonNullMember, walkJsonProperties } from "../../src/rules/model-utils.js"; +import { CompilerTester } from "../tester.js"; + +it("preserves inheritance overrides and encoded JSON names without payload filtering", async () => { + const { + program, + Derived: model, + Nullable: nullable, + Ambiguous: ambiguous, + OnlyNull: onlyNull, + } = await CompilerTester.compile(t.code` + model Base { @encodedName("application/json", "baseName") name: string; inherited: string; } + model ${t.model("Derived")} extends Base { @encodedName("application/json", "derivedName") name: string; } + union ${t.union("Nullable")} { Derived, null } + union ${t.union("Ambiguous")} { Derived, Base, null } + union ${t.union("OnlyNull")} { null } + `); + expect([...walkJsonProperties(program, model)].map(([name]) => name)).toEqual([ + "derivedName", + "inherited", + ]); + expect(getSingleNonNullMember(nullable)).toBe(model); + expect(getSingleNonNullMember(ambiguous)).toBeUndefined(); + expect(getSingleNonNullMember(onlyNull)).toBeUndefined(); +}); diff --git a/packages/typespec-azure-resource-manager/test/rules/no-repeated-path-info.test.ts b/packages/typespec-azure-resource-manager/test/rules/no-repeated-path-info.test.ts new file mode 100644 index 0000000000..9dd07343a7 --- /dev/null +++ b/packages/typespec-azure-resource-manager/test/rules/no-repeated-path-info.test.ts @@ -0,0 +1,342 @@ +import { Tester } from "#test/tester.js"; +import { getSourceLocation } from "@typespec/compiler"; +import { + createLinterRuleTester, + type LinterRuleTester, + type TesterInstance, +} from "@typespec/compiler/testing"; +import { beforeEach, describe, it } from "vitest"; +import { noRepeatedPathInfoRule } from "../../src/rules/no-repeated-path-info.js"; + +let runner: TesterInstance; + +let tester: LinterRuleTester; + +beforeEach(async () => { + runner = await Tester.createInstance(); + tester = createLinterRuleTester( + runner, + noRepeatedPathInfoRule, + "@azure-tools/typespec-azure-resource-manager", + ); +}); + +function diagnostic(name = "widgetName") { + return { + code: "@azure-tools/typespec-azure-resource-manager/no-repeated-path-info", + severity: "warning" as const, + message: `Request body property '${name}' repeats information already carried in the path or query.`, + }; +} + +function armResource(properties: string, parameters?: string) { + return ` + @armProviderNamespace + namespace Microsoft.Contoso; + + model Widget is TrackedResource { + @key("widgetName") @segment("widgets") @path name: string; + } + model WidgetProperties { ${properties} } + + @armResourceOperations + interface Widgets { + createOrUpdate is ArmResourceCreateOrReplaceSync; + } + `; +} + +const operation = ` + @put @route("/widgets/{widgetName}") + op create(@path widgetName: string, @body body: { properties: Properties }): void; +`; + +it("reports a path name repeated in ARM PUT resource properties", async () => { + await tester + .expect(armResource("/*repeated*/widgetName?: string;")) + .toEmitDiagnostics(({ repeated }) => ({ + ...diagnostic(), + pos: getSourceLocation(repeated).pos, + end: getSourceLocation(repeated).end, + })); +}); + +it("targets the repeated authored property, including inherited properties", async () => { + await tester + .expect( + ` + ${operation} + model Base { /*repeated*/widgetName?: string; } + model Properties extends Base {} + `, + ) + .toEmitDiagnostics(({ repeated }) => ({ + ...diagnostic(), + pos: getSourceLocation(repeated).pos, + end: getSourceLocation(repeated).end, + })); +}); + +it("reports a query name repeated in ARM PUT resource properties", async () => { + await tester + .expect(armResource("mode?: string;", "{ @query mode?: string; }")) + .toEmitDiagnostics([diagnostic("mode")]); +}); + +it("reports each repeated ARM path name", async () => { + await tester + .expect(armResource("widgetName?: string; resourceGroupName?: string;")) + .toEmitDiagnostics([diagnostic(), diagnostic("resourceGroupName")]); +}); + +it("reports a repeated tenant resource path name", async () => { + await tester + .expect( + ` + @armProviderNamespace + namespace Microsoft.Contoso; + @tenantResource + model TenantConfig is ProxyResource<{ configName?: string; setting?: string; }> { + @key("configName") @segment("configurations") @path name: string; + } + @armResourceOperations + interface TenantConfigs { + createOrUpdate is ArmResourceCreateOrReplaceSync< + TenantConfig, BaseParameters = Foundations.TenantBaseParameters + >; + } + `, + ) + .toEmitDiagnostics([diagnostic("configName")]); +}); + +it("allows ARM PUT properties with no repeated names", async () => { + await tester.expect(armResource("description?: string; sku?: string;")).toBeValid(); +}); + +it("ignores repeated top-level body properties", async () => { + await tester + .expect( + ` + @put @route("/widgets/{widgetName}") + op create(@path widgetName: string, + @body body: { widgetName?: string; properties: { description?: string } }): void; + `, + ) + .toBeValid(); +}); + +it("ignores repeated properties in PATCH bodies", async () => { + await tester + .expect( + ` + @patch @route("/widgets/{widgetName}") + op update(@path widgetName: string, + @body body: { properties: { widgetName?: string } }): void; + `, + ) + .toBeValid(); +}); + +it("reports a JSON alias that alone repeats a path name", async () => { + await tester + .expect(armResource('@encodedName("application/json", "widgetName") otherName?: string;')) + .toEmitDiagnostics([diagnostic()]); +}); + +it("allows an authored name when its JSON alias does not repeat a path name", async () => { + await tester + .expect(armResource('@encodedName("application/json", "otherName") widgetName?: string;')) + .toBeValid(); +}); + +it.each([false, true])( + "targets inherited JSON names while respecting an authored override: %s", + async (overrides) => { + const expectation = tester.expect(` + ${operation} + model Base { + @encodedName("application/json", "widgetName") /*repeated*/otherName?: string; + } + model Properties extends Base { + ${overrides ? '@encodedName("application/json", "description") otherName?: string;' : ""} + } + `); + if (overrides) { + await expectation.toBeValid(); + } else { + await expectation.toEmitDiagnostics(({ repeated }) => ({ + ...diagnostic(), + pos: getSourceLocation(repeated).pos, + end: getSourceLocation(repeated).end, + })); + } + }, +); + +it("compares JSON names with query wire names, not authored identifiers", async () => { + await tester + .expect( + ` + model Properties { + @encodedName("application/json", "wireQuery") otherName?: string; + @encodedName("application/json", "description") querySource?: string; + } + @put @route("/widgets") + op create(@query("wireQuery") querySource?: string, + @body body: { properties: Properties }): void; + `, + ) + .toEmitDiagnostics([diagnostic("wireQuery")]); +}); + +it("ignores encoded names for other media types", async () => { + await tester + .expect( + ` + ${operation} + model Properties { + @encodedName("application/xml", "widgetName") otherName?: string; + } + `, + ) + .toBeValid(); + await tester + .expect( + ` + ${operation} + model Properties { + @encodedName("application/xml", "description") widgetName?: string; + } + `, + ) + .toEmitDiagnostics([diagnostic()]); +}); + +it("uses supported HTTP parameter names rather than parameter source identifiers", async () => { + await tester + .expect( + ` + model Properties { wirePath?: string; wireQuery?: string; pathSource?: string; } + @put @route("/widgets/{wirePath}") + op create(@path("wirePath") pathSource: string, @query("wireQuery") querySource: string, + @body body: { properties: Properties }): void; + `, + ) + .toEmitDiagnostics([diagnostic("wirePath"), diagnostic("wireQuery")]); +}); + +it("does not recurse into cycles or shared sibling models", async () => { + await tester + .expect( + ` + ${operation} + model Nested { widgetName?: string; parent?: Properties; } + model Properties { self?: Properties; first?: Nested; second?: Nested; } + `, + ) + .toBeValid(); +}); + +it.each([false, true])( + "reports a shared declaration once per PUT operation with a JSON alias: %s", + async (encoded) => { + await tester + .expect( + ` + ${operation} + model Properties { + ${encoded ? '@encodedName("application/json", "widgetName")' : ""} + /*repeated*/${encoded ? "otherName" : "widgetName"}?: string; + } + @put @route("/other/{widgetName}") + op other(@path widgetName: string, @body body: { properties: Properties }): void; + `, + ) + .toEmitDiagnostics(({ repeated }) => + [1, 2].map(() => ({ + ...diagnostic(), + pos: getSourceLocation(repeated).pos, + end: getSourceLocation(repeated).end, + })), + ); + }, +); + +it("ignores missing bodies, non-model bodies, and missing or non-model properties bags", async () => { + await tester + .expect( + ` + @put @route("/none/{widgetName}") + op noBody(@path widgetName: string): void; + @put @route("/string/{widgetName}") + op stringBody(@path widgetName: string, @body body: string): void; + @put @route("/missing/{widgetName}") + op missingBag(@path widgetName: string, @body body: { description?: string }): void; + @put @route("/scalar/{widgetName}") + op scalarBag(@path widgetName: string, @body body: { properties: string }): void; + `, + ) + .toBeValid(); +}); + +it("checks ordinary project namespaces without provider metadata", async () => { + await tester + .expect(`namespace Contoso; ${operation} model Properties { widgetName?: string; }`) + .toEmitDiagnostics([diagnostic()]); +}); + +it.each([true, false])( + "checks concrete inherited PUT operations with repeated properties: %s", + async (repeats) => { + const expectation = tester.expect(` + model Properties { /*repeated*/${repeats ? "widgetName" : "description"}?: string; } + interface Templates { + @put @route("/widgets/{widgetName}") + op create(@path widgetName: string, @body body: T): void; + } + interface Widgets extends Templates<{ properties: Properties }> {} + `); + if (repeats) { + await expectation.toEmitDiagnostics(({ repeated }) => ({ + ...diagnostic(), + pos: getSourceLocation(repeated).pos, + end: getSourceLocation(repeated).end, + })); + } else { + await expectation.toBeValid(); + } + }, +); + +describe("URI and envelope name boundaries", () => { + it.each(["widgetName", "identity"])( + "selects the URI name independently of envelope names with @key(%s)", + async (parameterName) => { + await tester + .expect( + ` + @armProviderNamespace + namespace Microsoft.Contoso; + model Widget is TrackedResource { + @key("${parameterName}") @segment("widgets") @path name: string; + ...ManagedServiceIdentityProperty; + } + model WidgetProperties { + /*widgetName*/widgetName?: string; + /*identity*/identity?: string; + } + @armResourceOperations + interface Widgets { + createOrUpdate is ArmResourceCreateOrReplaceSync; + } + `, + ) + .toEmitDiagnostics((targets) => ({ + ...diagnostic(parameterName), + pos: getSourceLocation(targets[parameterName]).pos, + end: getSourceLocation(targets[parameterName]).end, + })); + }, + ); +}); diff --git a/packages/typespec-azure-resource-manager/test/tester.ts b/packages/typespec-azure-resource-manager/test/tester.ts index e25b3673f4..94f1fee6a8 100644 --- a/packages/typespec-azure-resource-manager/test/tester.ts +++ b/packages/typespec-azure-resource-manager/test/tester.ts @@ -1,6 +1,10 @@ import { resolvePath } from "@typespec/compiler"; import { createTester } from "@typespec/compiler/testing"; +export const CompilerTester = createTester(resolvePath(import.meta.dirname, ".."), { + libraries: [], +}); + export const Tester = createTester(resolvePath(import.meta.dirname, ".."), { libraries: [ "@typespec/http", diff --git a/packages/typespec-azure-rulesets/src/rulesets/resource-manager.ts b/packages/typespec-azure-rulesets/src/rulesets/resource-manager.ts index f5fef2dee8..33933fa75c 100644 --- a/packages/typespec-azure-rulesets/src/rulesets/resource-manager.ts +++ b/packages/typespec-azure-rulesets/src/rulesets/resource-manager.ts @@ -102,6 +102,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/no-query-in-post": false, + "@azure-tools/typespec-azure-resource-manager/no-repeated-path-info": false, "@azure-tools/typespec-azure-resource-manager/missing-operations-endpoint": true, "@azure-tools/typespec-azure-resource-manager/patch-envelope": true, "@azure-tools/typespec-azure-resource-manager/arm-resource-patch": 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 129723444f..e395bb3623 100644 --- a/website/src/content/docs/docs/howtos/ARM/arm-rules.md +++ b/website/src/content/docs/docs/howtos/ARM/arm-rules.md @@ -130,6 +130,7 @@ The tables below provide guidance to rule authors and ARM reviewers on how to ev | [`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-repeated-path-info`](https://azure.github.io/typespec-azure/docs/libraries/azure-resource-manager/rules/no-repeated-path-info/) | [`RepeatedPathInfo`](https://github.com/Azure/azure-openapi-validator/blob/main/docs/repeated-path-info.md) (partial) | **API, SDK.** ARM PUT resource properties repeat URI inputs, creating redundant SDK inputs and potentially conflicting values. | | [`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. | 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 7e2b168993..94cfacf19f 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 @@ -64,6 +64,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/no-query-in-post`](../rules/no-query-in-post.md) | ARM POST operations must not declare query parameters other than api-version. | +| [`@azure-tools/typespec-azure-resource-manager/no-repeated-path-info`](../rules/no-repeated-path-info.md) | ARM PUT request bodies must not repeat path or query parameters in the resource properties bag. | | [`@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/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. |