From 5454ad5b4e6d6b8c32b623c7f823557f30c77846 Mon Sep 17 00:00:00 2001 From: Haiyuan Zhang Date: Wed, 20 May 2026 01:18:17 +0800 Subject: [PATCH] Add arm-model-name-request-suffix linter rule (ARM package version) Experimental: implements the Request->Content model name rule in the ARM package instead of TCGC. This version cannot check @clientName overrides because the ARM package does not depend on TCGC, which causes false positives when client.tsp already has @@clientName fixes. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../src/linter.ts | 2 + .../rules/arm-model-name-request-suffix.ts | 61 +++++++++++++++++++ .../src/rulesets/resource-manager.ts | 1 + 3 files changed, 64 insertions(+) create mode 100644 packages/typespec-azure-resource-manager/src/rules/arm-model-name-request-suffix.ts diff --git a/packages/typespec-azure-resource-manager/src/linter.ts b/packages/typespec-azure-resource-manager/src/linter.ts index 1a027a612a..28bf7ef3a2 100644 --- a/packages/typespec-azure-resource-manager/src/linter.ts +++ b/packages/typespec-azure-resource-manager/src/linter.ts @@ -35,6 +35,7 @@ import { retryAfterRule } from "./rules/retry-after.js"; import { secretProprule } from "./rules/secret-prop.js"; import { unsupportedTypeRule } from "./rules/unsupported-type.js"; import { versionProgressionRule } from "./rules/version-progression.js"; +import { modelNameRequestSuffixRule } from "./rules/arm-model-name-request-suffix.js"; const rules = [ armNoRecordRule, @@ -73,6 +74,7 @@ const rules = [ unsupportedTypeRule, secretProprule, noEmptyModel, + modelNameRequestSuffixRule, ]; export const $linter = defineLinter({ diff --git a/packages/typespec-azure-resource-manager/src/rules/arm-model-name-request-suffix.ts b/packages/typespec-azure-resource-manager/src/rules/arm-model-name-request-suffix.ts new file mode 100644 index 0000000000..3c098d785f --- /dev/null +++ b/packages/typespec-azure-resource-manager/src/rules/arm-model-name-request-suffix.ts @@ -0,0 +1,61 @@ +import { createRule, defineCodeFix, getSourceLocation, Model, paramMessage } from "@typespec/compiler"; + +function createReplaceRequestWithContentCodeFix(target: Model, name: string) { + const newName = name.replace(/Request$/, "Content"); + return defineCodeFix({ + id: "replace-request-with-content", + label: `Add @clientName("${newName}", "csharp")`, + fix: (fixContext) => { + const location = getSourceLocation(target); + const text = location.file.text; + let lineStart = location.pos; + while (lineStart > 0 && text[lineStart - 1] !== "\n") { + lineStart--; + } + let indentEnd = lineStart; + while (indentEnd < text.length && (text[indentEnd] === " " || text[indentEnd] === "\t")) { + indentEnd++; + } + const indent = text.slice(lineStart, indentEnd); + const updatedLocation = { ...location, pos: lineStart }; + return fixContext.prependText( + updatedLocation, + `${indent}@clientName("${newName}", "csharp")\n`, + ); + }, + }); +} + +export const modelNameRequestSuffixRule = createRule({ + name: "arm-model-name-request-suffix", + description: "Model names should not end with 'Request'. Use 'Content' suffix instead.", + severity: "warning", + url: "https://azure.github.io/typespec-azure/docs/libraries/azure-resource-manager/rules/arm-model-name-request-suffix", + messages: { + default: paramMessage`Model name '${"name"}' ends with 'Request'. Consider renaming it to '${"suggestion"}' or use @clientName("${"suggestion"}", "csharp") to rename it for C#.`, + }, + create(context) { + // NOTE: Cannot use getLibraryName/createTCGCContext here because + // typespec-azure-resource-manager does not depend on typespec-client-generator-core. + // This means we can only check the raw TypeSpec model name, NOT the C#-scoped + // name from @clientName overrides. If a model has @clientName("FooContent", "csharp") + // but its TypeSpec name is still FooRequest, this rule will STILL flag it as a + // false positive. + return { + model: (model: Model) => { + if (typeof model.name !== "string") return; + const name = model.name; + if (!name.endsWith("Request")) return; + const suggestion = name.replace(/Request$/, "Content"); + context.reportDiagnostic({ + format: { + name, + suggestion, + }, + target: model, + codefixes: [createReplaceRequestWithContentCodeFix(model, name)], + }); + }, + }; + }, +}); diff --git a/packages/typespec-azure-rulesets/src/rulesets/resource-manager.ts b/packages/typespec-azure-rulesets/src/rulesets/resource-manager.ts index ba2b21abe0..c0862297b0 100644 --- a/packages/typespec-azure-rulesets/src/rulesets/resource-manager.ts +++ b/packages/typespec-azure-rulesets/src/rulesets/resource-manager.ts @@ -93,6 +93,7 @@ export default { "@azure-tools/typespec-azure-resource-manager/retry-after": false, // Disable https://github.com/Azure/typespec-azure/issues/3351 "@azure-tools/typespec-azure-resource-manager/secret-prop": true, "@azure-tools/typespec-azure-resource-manager/unsupported-type": true, + "@azure-tools/typespec-azure-resource-manager/arm-model-name-request-suffix": true, // TCGC rules "@azure-tools/typespec-client-generator-core/require-client-suffix": true,