Skip to content
Draft
Show file tree
Hide file tree
Changes from all 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
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
changeKind: feature
packages:
- "@azure-tools/typespec-client-generator-core"
- "@azure-tools/typespec-azure-rulesets"
---

Add `model-name-request-suffix` linter rule that flags model names ending with `Request` and suggests using `Content` suffix instead, per Azure SDK .NET naming conventions. Includes auto-fix via `@clientName`.
Original file line number Diff line number Diff line change
Expand Up @@ -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/model-name-request-suffix": true,
},
} satisfies LinterRuleSet;
10 changes: 8 additions & 2 deletions packages/typespec-client-generator-core/src/linter.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,17 @@
import { defineLinter } from "@typespec/compiler";
import { modelNameRequestSuffixRule } from "./rules/model-name-request-suffix.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,
modelNameRequestSuffixRule,
];

const csharpRules = [propertyNameConflictRule];
const csharpRules = [propertyNameConflictRule, modelNameRequestSuffixRule];

export const $linter = defineLinter({
rules,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import {
createRule,
defineCodeFix,
getSourceLocation,
Model,
paramMessage,
} from "@typespec/compiler";
import { createTCGCContext } from "../context.js";
import { getLibraryName } from "../public-utils.js";

function createReplaceRequestWithContentCodeFix(target: Model, csharpName: string) {
const newName = csharpName.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: "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/typespec-client-generator-core/rules/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) {
const tcgcContext = createTCGCContext(
context.program,
"@azure-tools/typespec-client-generator-core",
{
mutateNamespace: false,
},
);
return {
model: (model: Model) => {
const csharpName = getLibraryName(tcgcContext, model, "csharp");
if (!csharpName.endsWith("Request")) return;
const suggestion = csharpName.replace(/Request$/, "Content");
context.reportDiagnostic({
format: {
name: csharpName,
suggestion,
},
target: model,
codefixes: [createReplaceRequestWithContentCodeFix(model, csharpName)],
});
},
};
},
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
import {
createLinterRuleTester,
LinterRuleTester,
TesterInstance,
} from "@typespec/compiler/testing";
import { beforeEach, describe, it } from "vitest";
import { modelNameRequestSuffixRule } from "../../src/rules/model-name-request-suffix.rule.js";
import { SimpleTester } from "../tester.js";

let runner: TesterInstance;
let tester: LinterRuleTester;

beforeEach(async () => {
runner = await SimpleTester.createInstance();
tester = createLinterRuleTester(
runner,
modelNameRequestSuffixRule,
"@azure-tools/typespec-client-generator-core",
);
});

it("emits warning when model name ends with Request", async () => {
await tester
.expect(
`model PredictionRequest {
value: string;
}`,
)
.toEmitDiagnostics({
code: "@azure-tools/typespec-client-generator-core/model-name-request-suffix",
message: `Model name 'PredictionRequest' ends with 'Request'. Consider renaming it to 'PredictionContent' or use @clientName("PredictionContent", "csharp") to rename it for C#.`,
});
});

it("is valid when model name does not end with Request", async () => {
await tester
.expect(
`model PredictionContent {
value: string;
}`,
)
.toBeValid();
});

it("is valid when model name ends with Requests (plural)", async () => {
await tester
.expect(
`model BatchRequests {
items: string[];
}`,
)
.toBeValid();
});

it("is valid when @clientName overrides to Content suffix", async () => {
await tester
.expect(
`@clientName("PredictionContent", "csharp")
model PredictionRequest {
value: string;
}`,
)
.toBeValid();
});

it("emits warning when @clientName still ends with Request", async () => {
await tester
.expect(
`@clientName("MyPredictionRequest", "csharp")
model SomePrediction {
value: string;
}`,
)
.toEmitDiagnostics({
code: "@azure-tools/typespec-client-generator-core/model-name-request-suffix",
message: `Model name 'MyPredictionRequest' ends with 'Request'. Consider renaming it to 'MyPredictionContent' or use @clientName("MyPredictionContent", "csharp") to rename it for C#.`,
});
});

it("does not flag model named exactly Request", async () => {
await tester
.expect(
`model Request {
value: string;
}`,
)
.toEmitDiagnostics({
code: "@azure-tools/typespec-client-generator-core/model-name-request-suffix",
});
});

it("is case-sensitive - does not flag lowercase request", async () => {
await tester
.expect(
`model Myrequest {
value: string;
}`,
)
.toBeValid();
});

it("does not flag non-model types", async () => {
await tester.expect(`scalar RequestId extends string;`).toBeValid();
});

describe("codefix", () => {
it("offers replace Request with Content codefix", async () => {
await tester
.expect(
`
model PredictionRequest {
value: string;
}`,
)
.applyCodeFix("replace-request-with-content").toEqual(`
@clientName("PredictionContent", "csharp")
model PredictionRequest {
value: string;
}`);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
---
title: model-name-request-suffix
---

```text title="Full name"
@azure-tools/typespec-client-generator-core/model-name-request-suffix
```

Model names should not end with `Request`. 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)
requires using the `Content` suffix instead. This rule is part of the
`best-practices:csharp` ruleset.

The rule inspects the C#-scoped name of each model (the value of
`@clientName(..., "csharp")` if provided, otherwise the TypeSpec name).
If it ends with `Request`, a warning is emitted suggesting the `Content`
suffix instead.

To fix violations, either rename the model in TypeSpec when the
convention applies cross-language, or use
`@clientName("<Name>Content", "csharp")` to provide a C#-only override
while keeping the original TypeSpec name.

#### ❌ Incorrect

```tsp
model PredictionRequest {
value: string;
}

model CheckNameAvailabilityRequest {
name: string;
}
```

#### ✅ Correct (rename in TypeSpec)

```tsp
model PredictionContent {
value: string;
}

model CheckNameAvailabilityContent {
name: string;
}
```

#### ✅ Correct (rename for C# only)

```tsp
@clientName("PredictionContent", "csharp")
model PredictionRequest {
value: string;
}

@clientName("CheckNameAvailabilityContent", "csharp")
model CheckNameAvailabilityRequest {
name: string;
}
```
Loading