Skip to content
Merged
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
8 changes: 8 additions & 0 deletions .chronus/changes/no-case-mismatch-rule-2025-10-4-9-7-18.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
changeKind: feature
packages:
- "@azure-tools/typespec-azure-core"
- "@azure-tools/typespec-azure-rulesets"
---

Add new `no-case-mismatch` rule checking for types with names only differing by case
1 change: 1 addition & 0 deletions packages/typespec-azure-core/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ Available ruleSets:
| `@azure-tools/typespec-azure-core/composition-over-inheritance` | Check that if a model is used in an operation and has derived models that it has a discriminator or recommend to use composition via spread or `is`. |
| `@azure-tools/typespec-azure-core/known-encoding` | Check for supported encodings. |
| `@azure-tools/typespec-azure-core/long-running-polling-operation-required` | Long-running operations should have a linked polling operation. |
| [`@azure-tools/typespec-azure-core/no-case-mismatch`](https://azure.github.io/typespec-azure/docs/libraries/azure-core/rules/no-case-mismatch) | Validate that no two types have the same name with different casing. |
| [`@azure-tools/typespec-azure-core/no-closed-literal-union`](https://azure.github.io/typespec-azure/docs/libraries/azure-core/rules/no-closed-literal-union) | Unions of literals should include the base scalar type to mark them as open enum. |
| [`@azure-tools/typespec-azure-core/no-enum`](https://azure.github.io/typespec-azure/docs/libraries/azure-core/rules/no-enum) | Azure services should not use enums. |
| `@azure-tools/typespec-azure-core/no-error-status-codes` | Recommend using the error response defined by Azure REST API guidelines. |
Expand Down
2 changes: 2 additions & 0 deletions packages/typespec-azure-core/src/linter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { compositionOverInheritanceRule } from "./rules/composition-over-inherit
import { friendlyNameRule } from "./rules/friendly-name.js";
import { knownEncodingRule } from "./rules/known-encoding.js";
import { longRunningOperationsRequirePollingOperation } from "./rules/lro-polling-operation.js";
import { noCaseMismatchRule } from "./rules/no-case-mismatch.js";
import { noClosedLiteralUnionRule } from "./rules/no-closed-literal-union.js";
import { noEnumRule } from "./rules/no-enum.js";
import { noErrorStatusCodesRule } from "./rules/no-error-status-codes.js";
Expand Down Expand Up @@ -47,6 +48,7 @@ const rules = [
compositionOverInheritanceRule,
knownEncodingRule,
longRunningOperationsRequirePollingOperation,
noCaseMismatchRule,
noClosedLiteralUnionRule,
noEnumRule,
noErrorStatusCodesRule,
Expand Down
60 changes: 60 additions & 0 deletions packages/typespec-azure-core/src/rules/no-case-mismatch.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import {
type Enum,
type Model,
type Namespace,
type Union,
createRule,
isTemplateInstance,
paramMessage,
} from "@typespec/compiler";
import { DuplicateTracker } from "@typespec/compiler/utils";

type DataType = Model | Union | Enum;

export const noCaseMismatchRule = createRule({
name: "no-case-mismatch",
description: "Validate that no two types have the same name with different casing.",
severity: "warning",
url: "https://azure.github.io/typespec-azure/docs/libraries/azure-core/rules/no-case-mismatch",
messages: {
default: paramMessage`Type '${"typeName"}' has a name that differs only by casing from another type: ${"otherNames"}`,
},
create(context) {
const duplicateTrackers = new Map<Namespace, DuplicateTracker<string, DataType>>();

const track = (type: DataType) => {
if (!(type.namespace && type.name) || isTemplateInstance(type)) {
return;
}
let tracker = duplicateTrackers.get(type.namespace);
if (tracker === undefined) {
tracker = new DuplicateTracker<string, DataType>();
duplicateTrackers.set(type.namespace, tracker);
}
tracker.track(type.name.toLowerCase(), type);
};
return {
model: (en: Model) => track(en),
union: (en: Union) => track(en),
enum: (en: Enum) => track(en),
exit: () => {
for (const [_, tracker] of duplicateTrackers) {
for (const [_k, duplicates] of tracker.entries()) {
for (const duplicate of duplicates) {
context.reportDiagnostic({
format: {
typeName: duplicate.name!,
otherNames: duplicates
.map((d) => d.name)
.filter((name) => name !== duplicate.name)
.join(", "),
},
target: duplicate,
});
}
}
}
},
};
},
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import { Tester } from "#test/test-host.js";
import { LinterRuleTester, createLinterRuleTester } from "@typespec/compiler/testing";
import { beforeEach, describe, it } from "vitest";
import { noCaseMismatchRule } from "../../src/rules/no-case-mismatch.js";

let tester: LinterRuleTester;

beforeEach(async () => {
const runner = await Tester.createInstance();
tester = createLinterRuleTester(runner, noCaseMismatchRule, "@azure-tools/typespec-azure-core");
});

describe("flags models with names that just differ by casing", () => {
it.each(["model", "enum", "union"])("%s", async (type) => {
await tester
.expect(
`
${type} FailOverProperties {}
${type} FailoverProperties {}
`,
)
.toEmitDiagnostics([
{
code: "@azure-tools/typespec-azure-core/no-case-mismatch",
message:
"Type 'FailOverProperties' has a name that differs only by casing from another type: FailoverProperties",
},
{
code: "@azure-tools/typespec-azure-core/no-case-mismatch",
message:
"Type 'FailoverProperties' has a name that differs only by casing from another type: FailOverProperties",
},
]);
});
});

it("flags 3 or more types", async () => {
await tester
.expect(
`
model FailOverProperties {}
model FailoverProperties {}
model Failoverproperties {}
`,
)
.toEmitDiagnostics([
{
code: "@azure-tools/typespec-azure-core/no-case-mismatch",
message:
"Type 'FailOverProperties' has a name that differs only by casing from another type: FailoverProperties, Failoverproperties",
},
{
code: "@azure-tools/typespec-azure-core/no-case-mismatch",
message:
"Type 'FailoverProperties' has a name that differs only by casing from another type: FailOverProperties, Failoverproperties",
},
{
code: "@azure-tools/typespec-azure-core/no-case-mismatch",
message:
"Type 'Failoverproperties' has a name that differs only by casing from another type: FailOverProperties, FailoverProperties",
},
]);
});

it("doesn't flag if names are differ by more than casing", async () => {
await tester
.expect(
`
model FailedOver {}
model FailOver {}
`,
)
.toBeValid();
});

it("doesn't flag template instances", async () => {
await tester
.expect(
`
model Template<T> {
t: T;
}

model Test {
a: Template<string>;
b: Template<int32>;
}
`,
)
.toBeValid();
});
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export default {
"@azure-tools/typespec-azure-core/use-extensible-enum": true,
"@azure-tools/typespec-azure-core/known-encoding": true,
"@azure-tools/typespec-azure-core/long-running-polling-operation-required": true,
"@azure-tools/typespec-azure-core/no-case-mismatch": true,
"@azure-tools/typespec-azure-core/no-closed-literal-union": true,
"@azure-tools/typespec-azure-core/no-enum": true,
"@azure-tools/typespec-azure-core/no-error-status-codes": true,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export default {
"@azure-tools/typespec-azure-core/use-extensible-enum": true,
"@azure-tools/typespec-azure-core/known-encoding": true,
"@azure-tools/typespec-azure-core/long-running-polling-operation-required": true,
"@azure-tools/typespec-azure-core/no-case-mismatch": true,
"@azure-tools/typespec-azure-core/no-closed-literal-union": true,
"@azure-tools/typespec-azure-core/no-enum": true,
"@azure-tools/typespec-azure-core/no-error-status-codes": true,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ Available ruleSets:
| `@azure-tools/typespec-azure-core/composition-over-inheritance` | Check that if a model is used in an operation and has derived models that it has a discriminator or recommend to use composition via spread or `is`. |
| `@azure-tools/typespec-azure-core/known-encoding` | Check for supported encodings. |
| `@azure-tools/typespec-azure-core/long-running-polling-operation-required` | Long-running operations should have a linked polling operation. |
| [`@azure-tools/typespec-azure-core/no-case-mismatch`](/libraries/azure-core/rules/no-case-mismatch.md) | Validate that no two types have the same name with different casing. |
| [`@azure-tools/typespec-azure-core/no-closed-literal-union`](/libraries/azure-core/rules/no-closed-literal-union.md) | Unions of literals should include the base scalar type to mark them as open enum. |
| [`@azure-tools/typespec-azure-core/no-enum`](/libraries/azure-core/rules/no-enum.md) | Azure services should not use enums. |
| `@azure-tools/typespec-azure-core/no-error-status-codes` | Recommend using the error response defined by Azure REST API guidelines. |
Expand Down
Loading