Skip to content

Commit c9b4ace

Browse files
Add no case mismatch rule (#3475)
fix [#3394](#3394) --------- Co-authored-by: Mark Cowlishaw <[email protected]>
1 parent 19ed2e0 commit c9b4ace

File tree

8 files changed

+165
-0
lines changed

8 files changed

+165
-0
lines changed
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
---
2+
changeKind: feature
3+
packages:
4+
- "@azure-tools/typespec-azure-core"
5+
- "@azure-tools/typespec-azure-rulesets"
6+
---
7+
8+
Add new `no-case-mismatch` rule checking for types with names only differing by case

packages/typespec-azure-core/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ Available ruleSets:
3737
| `@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`. |
3838
| `@azure-tools/typespec-azure-core/known-encoding` | Check for supported encodings. |
3939
| `@azure-tools/typespec-azure-core/long-running-polling-operation-required` | Long-running operations should have a linked polling operation. |
40+
| [`@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. |
4041
| [`@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. |
4142
| [`@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. |
4243
| `@azure-tools/typespec-azure-core/no-error-status-codes` | Recommend using the error response defined by Azure REST API guidelines. |

packages/typespec-azure-core/src/linter.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { compositionOverInheritanceRule } from "./rules/composition-over-inherit
77
import { friendlyNameRule } from "./rules/friendly-name.js";
88
import { knownEncodingRule } from "./rules/known-encoding.js";
99
import { longRunningOperationsRequirePollingOperation } from "./rules/lro-polling-operation.js";
10+
import { noCaseMismatchRule } from "./rules/no-case-mismatch.js";
1011
import { noClosedLiteralUnionRule } from "./rules/no-closed-literal-union.js";
1112
import { noEnumRule } from "./rules/no-enum.js";
1213
import { noErrorStatusCodesRule } from "./rules/no-error-status-codes.js";
@@ -47,6 +48,7 @@ const rules = [
4748
compositionOverInheritanceRule,
4849
knownEncodingRule,
4950
longRunningOperationsRequirePollingOperation,
51+
noCaseMismatchRule,
5052
noClosedLiteralUnionRule,
5153
noEnumRule,
5254
noErrorStatusCodesRule,
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import {
2+
type Enum,
3+
type Model,
4+
type Namespace,
5+
type Union,
6+
createRule,
7+
isTemplateInstance,
8+
paramMessage,
9+
} from "@typespec/compiler";
10+
import { DuplicateTracker } from "@typespec/compiler/utils";
11+
12+
type DataType = Model | Union | Enum;
13+
14+
export const noCaseMismatchRule = createRule({
15+
name: "no-case-mismatch",
16+
description: "Validate that no two types have the same name with different casing.",
17+
severity: "warning",
18+
url: "https://azure.github.io/typespec-azure/docs/libraries/azure-core/rules/no-case-mismatch",
19+
messages: {
20+
default: paramMessage`Type '${"typeName"}' has a name that differs only by casing from another type: ${"otherNames"}`,
21+
},
22+
create(context) {
23+
const duplicateTrackers = new Map<Namespace, DuplicateTracker<string, DataType>>();
24+
25+
const track = (type: DataType) => {
26+
if (!(type.namespace && type.name) || isTemplateInstance(type)) {
27+
return;
28+
}
29+
let tracker = duplicateTrackers.get(type.namespace);
30+
if (tracker === undefined) {
31+
tracker = new DuplicateTracker<string, DataType>();
32+
duplicateTrackers.set(type.namespace, tracker);
33+
}
34+
tracker.track(type.name.toLowerCase(), type);
35+
};
36+
return {
37+
model: (en: Model) => track(en),
38+
union: (en: Union) => track(en),
39+
enum: (en: Enum) => track(en),
40+
exit: () => {
41+
for (const [_, tracker] of duplicateTrackers) {
42+
for (const [_k, duplicates] of tracker.entries()) {
43+
for (const duplicate of duplicates) {
44+
context.reportDiagnostic({
45+
format: {
46+
typeName: duplicate.name!,
47+
otherNames: duplicates
48+
.map((d) => d.name)
49+
.filter((name) => name !== duplicate.name)
50+
.join(", "),
51+
},
52+
target: duplicate,
53+
});
54+
}
55+
}
56+
}
57+
},
58+
};
59+
},
60+
});
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
import { Tester } from "#test/test-host.js";
2+
import { LinterRuleTester, createLinterRuleTester } from "@typespec/compiler/testing";
3+
import { beforeEach, describe, it } from "vitest";
4+
import { noCaseMismatchRule } from "../../src/rules/no-case-mismatch.js";
5+
6+
let tester: LinterRuleTester;
7+
8+
beforeEach(async () => {
9+
const runner = await Tester.createInstance();
10+
tester = createLinterRuleTester(runner, noCaseMismatchRule, "@azure-tools/typespec-azure-core");
11+
});
12+
13+
describe("flags models with names that just differ by casing", () => {
14+
it.each(["model", "enum", "union"])("%s", async (type) => {
15+
await tester
16+
.expect(
17+
`
18+
${type} FailOverProperties {}
19+
${type} FailoverProperties {}
20+
`,
21+
)
22+
.toEmitDiagnostics([
23+
{
24+
code: "@azure-tools/typespec-azure-core/no-case-mismatch",
25+
message:
26+
"Type 'FailOverProperties' has a name that differs only by casing from another type: FailoverProperties",
27+
},
28+
{
29+
code: "@azure-tools/typespec-azure-core/no-case-mismatch",
30+
message:
31+
"Type 'FailoverProperties' has a name that differs only by casing from another type: FailOverProperties",
32+
},
33+
]);
34+
});
35+
});
36+
37+
it("flags 3 or more types", async () => {
38+
await tester
39+
.expect(
40+
`
41+
model FailOverProperties {}
42+
model FailoverProperties {}
43+
model Failoverproperties {}
44+
`,
45+
)
46+
.toEmitDiagnostics([
47+
{
48+
code: "@azure-tools/typespec-azure-core/no-case-mismatch",
49+
message:
50+
"Type 'FailOverProperties' has a name that differs only by casing from another type: FailoverProperties, Failoverproperties",
51+
},
52+
{
53+
code: "@azure-tools/typespec-azure-core/no-case-mismatch",
54+
message:
55+
"Type 'FailoverProperties' has a name that differs only by casing from another type: FailOverProperties, Failoverproperties",
56+
},
57+
{
58+
code: "@azure-tools/typespec-azure-core/no-case-mismatch",
59+
message:
60+
"Type 'Failoverproperties' has a name that differs only by casing from another type: FailOverProperties, FailoverProperties",
61+
},
62+
]);
63+
});
64+
65+
it("doesn't flag if names are differ by more than casing", async () => {
66+
await tester
67+
.expect(
68+
`
69+
model FailedOver {}
70+
model FailOver {}
71+
`,
72+
)
73+
.toBeValid();
74+
});
75+
76+
it("doesn't flag template instances", async () => {
77+
await tester
78+
.expect(
79+
`
80+
model Template<T> {
81+
t: T;
82+
}
83+
84+
model Test {
85+
a: Template<string>;
86+
b: Template<int32>;
87+
}
88+
`,
89+
)
90+
.toBeValid();
91+
});

packages/typespec-azure-rulesets/src/rulesets/data-plane.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ export default {
1111
"@azure-tools/typespec-azure-core/use-extensible-enum": true,
1212
"@azure-tools/typespec-azure-core/known-encoding": true,
1313
"@azure-tools/typespec-azure-core/long-running-polling-operation-required": true,
14+
"@azure-tools/typespec-azure-core/no-case-mismatch": true,
1415
"@azure-tools/typespec-azure-core/no-closed-literal-union": true,
1516
"@azure-tools/typespec-azure-core/no-enum": true,
1617
"@azure-tools/typespec-azure-core/no-error-status-codes": true,

packages/typespec-azure-rulesets/src/rulesets/resource-manager.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ export default {
1212
"@azure-tools/typespec-azure-core/use-extensible-enum": true,
1313
"@azure-tools/typespec-azure-core/known-encoding": true,
1414
"@azure-tools/typespec-azure-core/long-running-polling-operation-required": true,
15+
"@azure-tools/typespec-azure-core/no-case-mismatch": true,
1516
"@azure-tools/typespec-azure-core/no-closed-literal-union": true,
1617
"@azure-tools/typespec-azure-core/no-enum": true,
1718
"@azure-tools/typespec-azure-core/no-error-status-codes": true,

website/src/content/docs/docs/libraries/azure-core/reference/linter.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ Available ruleSets:
3131
| `@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`. |
3232
| `@azure-tools/typespec-azure-core/known-encoding` | Check for supported encodings. |
3333
| `@azure-tools/typespec-azure-core/long-running-polling-operation-required` | Long-running operations should have a linked polling operation. |
34+
| [`@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. |
3435
| [`@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. |
3536
| [`@azure-tools/typespec-azure-core/no-enum`](/libraries/azure-core/rules/no-enum.md) | Azure services should not use enums. |
3637
| `@azure-tools/typespec-azure-core/no-error-status-codes` | Recommend using the error response defined by Azure REST API guidelines. |

0 commit comments

Comments
 (0)