From d8d097dea541a76f47f8ace2c793dd3d1e0945be Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 19 May 2026 07:58:18 +0000 Subject: [PATCH 1/4] Initial plan From b2d0c030162567c07b5429921ca0360a8801d98e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 19 May 2026 08:07:48 +0000 Subject: [PATCH 2/4] Add bool-property-name-prefix lint rule for TCGC csharp ruleset Agent-Logs-Url: https://github.com/Azure/typespec-azure/sessions/58d9eeb1-3afb-4fbc-8cc4-84460049bbf8 Co-authored-by: haiyuazhang <1924967+haiyuazhang@users.noreply.github.com> --- ...l-property-name-prefix-2026-5-19-7-50-0.md | 7 + .../src/linter.ts | 10 +- .../rules/bool-property-name-prefix.rule.ts | 70 +++++++++ .../rules/bool-property-name-prefix.test.ts | 147 ++++++++++++++++++ .../rules/bool-property-name-prefix.md | 57 +++++++ 5 files changed, 289 insertions(+), 2 deletions(-) create mode 100644 .chronus/changes/feature-tcgc-bool-property-name-prefix-2026-5-19-7-50-0.md create mode 100644 packages/typespec-client-generator-core/src/rules/bool-property-name-prefix.rule.ts create mode 100644 packages/typespec-client-generator-core/test/rules/bool-property-name-prefix.test.ts create mode 100644 website/src/content/docs/docs/libraries/typespec-client-generator-core/rules/bool-property-name-prefix.md diff --git a/.chronus/changes/feature-tcgc-bool-property-name-prefix-2026-5-19-7-50-0.md b/.chronus/changes/feature-tcgc-bool-property-name-prefix-2026-5-19-7-50-0.md new file mode 100644 index 0000000000..45233ddf2f --- /dev/null +++ b/.chronus/changes/feature-tcgc-bool-property-name-prefix-2026-5-19-7-50-0.md @@ -0,0 +1,7 @@ +--- +changeKind: feature +packages: + - "@azure-tools/typespec-client-generator-core" +--- + +Add new `bool-property-name-prefix` lint rule (enabled in the `best-practices:csharp` ruleset) that flags boolean properties and operation parameters whose names do not start with a verb prefix such as `Is`, `Has`, `Can`, `Should`, `Are`, `Was`, `Will`, `Do`, or `Does` followed by an uppercase letter, following the Azure SDK for .NET naming conventions. diff --git a/packages/typespec-client-generator-core/src/linter.ts b/packages/typespec-client-generator-core/src/linter.ts index 737455f880..0fbded6cd9 100644 --- a/packages/typespec-client-generator-core/src/linter.ts +++ b/packages/typespec-client-generator-core/src/linter.ts @@ -1,11 +1,17 @@ import { defineLinter } from "@typespec/compiler"; +import { boolPropertyNamePrefixRule } from "./rules/bool-property-name-prefix.rule.js"; import { noUnnamedTypesRule } from "./rules/no-unnamed-types.rule.js"; import { propertyNameConflictRule } from "./rules/property-name-conflict.rule.js"; import { requireClientSuffixRule } from "./rules/require-client-suffix.rule.js"; -const rules = [requireClientSuffixRule, propertyNameConflictRule, noUnnamedTypesRule]; +const rules = [ + requireClientSuffixRule, + propertyNameConflictRule, + noUnnamedTypesRule, + boolPropertyNamePrefixRule, +]; -const csharpRules = [propertyNameConflictRule]; +const csharpRules = [propertyNameConflictRule, boolPropertyNamePrefixRule]; export const $linter = defineLinter({ rules, diff --git a/packages/typespec-client-generator-core/src/rules/bool-property-name-prefix.rule.ts b/packages/typespec-client-generator-core/src/rules/bool-property-name-prefix.rule.ts new file mode 100644 index 0000000000..05f99f36b3 --- /dev/null +++ b/packages/typespec-client-generator-core/src/rules/bool-property-name-prefix.rule.ts @@ -0,0 +1,70 @@ +import { createRule, ModelProperty, paramMessage, Type } from "@typespec/compiler"; +import { createTCGCContext } from "../context.js"; +import { getLibraryName } from "../public-utils.js"; + +const ALLOWED_PREFIXES = ["Is", "Has", "Can", "Should", "Are", "Was", "Will", "Does", "Do"]; + +function isBooleanType(type: Type): boolean { + if (type.kind !== "Scalar") return false; + let current: Type | undefined = type; + while (current && current.kind === "Scalar") { + if (current.name === "boolean") return true; + current = current.baseScalar; + } + return false; +} + +function startsWithAllowedPrefix(name: string): boolean { + for (const prefix of ALLOWED_PREFIXES) { + if (name.length > prefix.length && name.startsWith(prefix)) { + const next = name.charAt(prefix.length); + if (next >= "A" && next <= "Z") { + return true; + } + } + } + return false; +} + +function toPascalCase(name: string): string { + if (name.length === 0) return name; + return name.charAt(0).toUpperCase() + name.slice(1); +} + +export const boolPropertyNamePrefixRule = createRule({ + name: "bool-property-name-prefix", + description: + "Boolean property and parameter names should start with a verb prefix such as Is, Has, Can, Should, etc.", + severity: "warning", + url: "https://azure.github.io/typespec-azure/docs/libraries/typespec-client-generator-core/rules/bool-property-name-prefix", + messages: { + default: paramMessage`Boolean property or parameter '${"name"}' should start with one of the following verb prefixes followed by an uppercase letter: ${"prefixes"}. Consider renaming it (for example to '${"suggestion"}') or use @clientName("${"suggestion"}", "csharp") to rename it for C#.`, + }, + create(context) { + const tcgcContext = createTCGCContext( + context.program, + "@azure-tools/typespec-client-generator-core", + { + mutateNamespace: false, + }, + ); + return { + modelProperty: (property: ModelProperty) => { + if (!isBooleanType(property.type)) return; + // The C# emitter PascalCases property/parameter names, so apply the same + // transformation before checking for an allowed verb prefix. + const csharpName = toPascalCase(getLibraryName(tcgcContext, property, "csharp")); + if (startsWithAllowedPrefix(csharpName)) return; + const suggestion = `Is${csharpName}`; + context.reportDiagnostic({ + format: { + name: csharpName, + prefixes: ALLOWED_PREFIXES.join(", "), + suggestion, + }, + target: property, + }); + }, + }; + }, +}); diff --git a/packages/typespec-client-generator-core/test/rules/bool-property-name-prefix.test.ts b/packages/typespec-client-generator-core/test/rules/bool-property-name-prefix.test.ts new file mode 100644 index 0000000000..5e65e00adc --- /dev/null +++ b/packages/typespec-client-generator-core/test/rules/bool-property-name-prefix.test.ts @@ -0,0 +1,147 @@ +import { + createLinterRuleTester, + LinterRuleTester, + TesterInstance, +} from "@typespec/compiler/testing"; +import { beforeEach, it } from "vitest"; +import { boolPropertyNamePrefixRule } from "../../src/rules/bool-property-name-prefix.rule.js"; +import { SimpleTester } from "../tester.js"; + +let runner: TesterInstance; +let tester: LinterRuleTester; + +beforeEach(async () => { + runner = await SimpleTester.createInstance(); + tester = createLinterRuleTester( + runner, + boolPropertyNamePrefixRule, + "@azure-tools/typespec-client-generator-core", + ); +}); + +it("emits warning when a boolean property has no verb prefix", async () => { + await tester + .expect( + `model Foo { + tracked: boolean; + }`, + ) + .toEmitDiagnostics({ + code: "@azure-tools/typespec-client-generator-core/bool-property-name-prefix", + message: `Boolean property or parameter 'Tracked' should start with one of the following verb prefixes followed by an uppercase letter: Is, Has, Can, Should, Are, Was, Will, Does, Do. Consider renaming it (for example to 'IsTracked') or use @clientName("IsTracked", "csharp") to rename it for C#.`, + }); +}); + +it("is valid when boolean property starts with Is", async () => { + await tester + .expect( + `model Foo { + isTracked: boolean; + }`, + ) + .toBeValid(); +}); + +it("is valid when boolean property starts with Has", async () => { + await tester + .expect( + `model Foo { + hasDynamicFieldSchema: boolean; + }`, + ) + .toBeValid(); +}); + +it("is valid when boolean property starts with Can", async () => { + await tester + .expect( + `model Foo { + canDelete: boolean; + }`, + ) + .toBeValid(); +}); + +it("is valid when boolean property starts with Should", async () => { + await tester + .expect( + `model Foo { + shouldRetry: boolean; + }`, + ) + .toBeValid(); +}); + +it("does not emit for non-boolean properties", async () => { + await tester + .expect( + `model Foo { + name: string; + count: int32; + }`, + ) + .toBeValid(); +}); + +it("emits warning for properties whose type extends boolean", async () => { + await tester + .expect( + `scalar MyBool extends boolean; + model Foo { + tracked: MyBool; + }`, + ) + .toEmitDiagnostics({ + code: "@azure-tools/typespec-client-generator-core/bool-property-name-prefix", + message: `Boolean property or parameter 'Tracked' should start with one of the following verb prefixes followed by an uppercase letter: Is, Has, Can, Should, Are, Was, Will, Does, Do. Consider renaming it (for example to 'IsTracked') or use @clientName("IsTracked", "csharp") to rename it for C#.`, + }); +}); + +it(`is valid when @clientName("IsTracked", "csharp") provides csharp-specific name with prefix`, async () => { + await tester + .expect( + `model Foo { + @clientName("IsTracked", "csharp") + tracked: boolean; + }`, + ) + .toBeValid(); +}); + +it(`emits warning when @clientName provides csharp name without prefix`, async () => { + await tester + .expect( + `model Foo { + @clientName("renamed", "csharp") + isTracked: boolean; + }`, + ) + .toEmitDiagnostics({ + code: "@azure-tools/typespec-client-generator-core/bool-property-name-prefix", + message: `Boolean property or parameter 'Renamed' should start with one of the following verb prefixes followed by an uppercase letter: Is, Has, Can, Should, Are, Was, Will, Does, Do. Consider renaming it (for example to 'IsRenamed') or use @clientName("IsRenamed", "csharp") to rename it for C#.`, + }); +}); + +it("emits warning for boolean operation parameter without prefix", async () => { + await tester.expect(`op getData(tracked: boolean): void;`).toEmitDiagnostics({ + code: "@azure-tools/typespec-client-generator-core/bool-property-name-prefix", + message: `Boolean property or parameter 'Tracked' should start with one of the following verb prefixes followed by an uppercase letter: Is, Has, Can, Should, Are, Was, Will, Does, Do. Consider renaming it (for example to 'IsTracked') or use @clientName("IsTracked", "csharp") to rename it for C#.`, + }); +}); + +it("is valid for boolean operation parameter with prefix", async () => { + await tester.expect(`op getData(isTracked: boolean): void;`).toBeValid(); +}); + +it("does not match prefix substrings without uppercase boundary", async () => { + // "issue" starts with "is" but next char is lowercase -> still invalid + await tester + .expect( + `model Foo { + issue: boolean; + }`, + ) + .toEmitDiagnostics({ + code: "@azure-tools/typespec-client-generator-core/bool-property-name-prefix", + }); +}); diff --git a/website/src/content/docs/docs/libraries/typespec-client-generator-core/rules/bool-property-name-prefix.md b/website/src/content/docs/docs/libraries/typespec-client-generator-core/rules/bool-property-name-prefix.md new file mode 100644 index 0000000000..69983f0f94 --- /dev/null +++ b/website/src/content/docs/docs/libraries/typespec-client-generator-core/rules/bool-property-name-prefix.md @@ -0,0 +1,57 @@ +--- +title: bool-property-name-prefix +--- + +```text title="Full name" +@azure-tools/typespec-client-generator-core/bool-property-name-prefix +``` + +Boolean property and parameter names should start with a verb such as +`Is`, `Has`, `Can`, `Should`, `Are`, `Was`, `Will`, `Does`, or `Do`, +followed by an uppercase letter. This rule encodes the +[Azure SDK for .NET management plane naming convention](https://github.com/Azure/azure-sdk-for-net/blob/main/doc/dev/Mgmt-Naming-Conventions.md) +and is part of the `best-practices:csharp` ruleset. + +The rule walks model properties and operation parameters whose effective +type resolves to `boolean` (including custom scalars that extend +`boolean`), and inspects the C#-scoped name (the value of +`@clientName(..., "csharp")` if provided, otherwise the TypeSpec name). + +To fix violations, either rename the property/parameter in TypeSpec when +the convention applies cross-language, or use +`@clientName("Is", "csharp")` to provide a C#-only override +while keeping the original TypeSpec name. + +#### ❌ Incorrect + +```tsp +model Widget { + tracked: boolean; + exclude: boolean; +} + +op getWidget(restore: boolean): Widget; +``` + +#### ✅ Correct (rename in TypeSpec) + +```tsp +model Widget { + isTracked: boolean; + isExcluded: boolean; +} + +op getWidget(isRestore: boolean): Widget; +``` + +#### ✅ Correct (rename for C# only) + +```tsp +model Widget { + @clientName("IsTracked", "csharp") + tracked: boolean; + + @clientName("IsExcluded", "csharp") + exclude: boolean; +} +``` From 1c7f94f37bb8b91599faddd4290521463261bc1e Mon Sep 17 00:00:00 2001 From: Haiyuan Zhang Date: Wed, 20 May 2026 00:00:51 +0800 Subject: [PATCH 3/4] Add codefixes for bool-property-name-prefix rule Offer three Quick Fix options (Is, Can, Has prefix) when the rule flags a boolean property. Each codefix inserts @clientName with the chosen prefix on the line before the property. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../rules/bool-property-name-prefix.rule.ts | 44 ++++++++++++++++- .../rules/bool-property-name-prefix.test.ts | 49 ++++++++++++++++++- 2 files changed, 91 insertions(+), 2 deletions(-) diff --git a/packages/typespec-client-generator-core/src/rules/bool-property-name-prefix.rule.ts b/packages/typespec-client-generator-core/src/rules/bool-property-name-prefix.rule.ts index 05f99f36b3..dc5be578d7 100644 --- a/packages/typespec-client-generator-core/src/rules/bool-property-name-prefix.rule.ts +++ b/packages/typespec-client-generator-core/src/rules/bool-property-name-prefix.rule.ts @@ -1,8 +1,17 @@ -import { createRule, ModelProperty, paramMessage, Type } from "@typespec/compiler"; +import { + CodeFix, + createRule, + defineCodeFix, + getSourceLocation, + ModelProperty, + paramMessage, + Type, +} from "@typespec/compiler"; import { createTCGCContext } from "../context.js"; import { getLibraryName } from "../public-utils.js"; const ALLOWED_PREFIXES = ["Is", "Has", "Can", "Should", "Are", "Was", "Will", "Does", "Do"]; +const SUGGESTED_PREFIXES = ["Is", "Can", "Has"] as const; function isBooleanType(type: Type): boolean { if (type.kind !== "Scalar") return false; @@ -31,6 +40,36 @@ function toPascalCase(name: string): string { return name.charAt(0).toUpperCase() + name.slice(1); } +function createClientNameCodeFix( + target: ModelProperty, + prefix: string, + csharpName: string, +): CodeFix { + const newName = `${prefix}${csharpName}`; + return defineCodeFix({ + id: `add-clientName-${prefix}`, + 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 boolPropertyNamePrefixRule = createRule({ name: "bool-property-name-prefix", description: @@ -63,6 +102,9 @@ export const boolPropertyNamePrefixRule = createRule({ suggestion, }, target: property, + codefixes: SUGGESTED_PREFIXES.map((prefix) => + createClientNameCodeFix(property, prefix, csharpName), + ), }); }, }; diff --git a/packages/typespec-client-generator-core/test/rules/bool-property-name-prefix.test.ts b/packages/typespec-client-generator-core/test/rules/bool-property-name-prefix.test.ts index 5e65e00adc..a5a54ad74b 100644 --- a/packages/typespec-client-generator-core/test/rules/bool-property-name-prefix.test.ts +++ b/packages/typespec-client-generator-core/test/rules/bool-property-name-prefix.test.ts @@ -3,7 +3,7 @@ import { LinterRuleTester, TesterInstance, } from "@typespec/compiler/testing"; -import { beforeEach, it } from "vitest"; +import { beforeEach, describe, it } from "vitest"; import { boolPropertyNamePrefixRule } from "../../src/rules/bool-property-name-prefix.rule.js"; import { SimpleTester } from "../tester.js"; @@ -145,3 +145,50 @@ it("does not match prefix substrings without uppercase boundary", async () => { code: "@azure-tools/typespec-client-generator-core/bool-property-name-prefix", }); }); + +describe("codefix", () => { + it("offers Is prefix codefix", async () => { + await tester + .expect( + ` + model Foo { + tracked: boolean; + }`, + ) + .applyCodeFix("add-clientName-Is").toEqual(` + model Foo { + @clientName("IsTracked", "csharp") + tracked: boolean; + }`); + }); + + it("offers Can prefix codefix", async () => { + await tester + .expect( + ` + model Foo { + tracked: boolean; + }`, + ) + .applyCodeFix("add-clientName-Can").toEqual(` + model Foo { + @clientName("CanTracked", "csharp") + tracked: boolean; + }`); + }); + + it("offers Has prefix codefix", async () => { + await tester + .expect( + ` + model Foo { + tracked: boolean; + }`, + ) + .applyCodeFix("add-clientName-Has").toEqual(` + model Foo { + @clientName("HasTracked", "csharp") + tracked: boolean; + }`); + }); +}); From 8fe365f14e40204088e8da522e3b1a4d0ef25549 Mon Sep 17 00:00:00 2001 From: Haiyuan Zhang Date: Wed, 20 May 2026 00:31:27 +0800 Subject: [PATCH 4/4] Add bool-property-name-prefix rule to resource-manager mega-ruleset Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../typespec-azure-rulesets/src/rulesets/resource-manager.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/typespec-azure-rulesets/src/rulesets/resource-manager.ts b/packages/typespec-azure-rulesets/src/rulesets/resource-manager.ts index ba2b21abe0..71df81c911 100644 --- a/packages/typespec-azure-rulesets/src/rulesets/resource-manager.ts +++ b/packages/typespec-azure-rulesets/src/rulesets/resource-manager.ts @@ -98,5 +98,6 @@ export default { "@azure-tools/typespec-client-generator-core/require-client-suffix": true, "@azure-tools/typespec-client-generator-core/property-name-conflict": true, "@azure-tools/typespec-client-generator-core/no-unnamed-types": false, // Too bad performance https://github.com/Azure/typespec-azure/issues/2803 + "@azure-tools/typespec-client-generator-core/bool-property-name-prefix": true, }, } satisfies LinterRuleSet;