Skip to content

Commit b5a5fe4

Browse files
timreichenkt3k
andauthored
chore: add naming-convention Deno Style Guide linter plugin (#6559)
Co-authored-by: Yoshiya Hinosawa <[email protected]>
1 parent bc71968 commit b5a5fe4

13 files changed

+314
-45
lines changed

Diff for: _tools/lint_plugin.ts

+149
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,24 @@
66
* {@link https://docs.deno.com/runtime/contributing/style_guide/ | Deno Style Guide}
77
*/
88

9+
import { toCamelCase, toPascalCase } from "@std/text";
10+
11+
const PASCAL_CASE_REGEXP = /^_?(?:[A-Z][a-z0-9]*)*_?$/;
12+
const UPPER_CASE_ONLY = /^_?[A-Z]{2,}$/;
13+
function isPascalCase(string: string): boolean {
14+
return PASCAL_CASE_REGEXP.test(string) && !UPPER_CASE_ONLY.test(string);
15+
}
16+
17+
const CAMEL_CASE_REGEXP = /^[_a-z][a-z0-9]*(?:[A-Z][a-z0-9]*)*_?$/;
18+
function isCamelCase(string: string): boolean {
19+
return CAMEL_CASE_REGEXP.test(string);
20+
}
21+
22+
const CONSTANT_CASE_REGEXP = /^[A-Z][A-Z0-9]*(?:_[A-Z0-9]+)*_?$/;
23+
function isConstantCase(string: string): boolean {
24+
return CONSTANT_CASE_REGEXP.test(string);
25+
}
26+
927
export default {
1028
name: "deno-style-guide",
1129
rules: {
@@ -38,5 +56,136 @@ export default {
3856
};
3957
},
4058
},
59+
// https://docs.deno.com/runtime/contributing/style_guide/#naming-convention/
60+
"naming-convention": {
61+
create(context) {
62+
return {
63+
TSTypeAliasDeclaration(node) {
64+
const name = node.id.name;
65+
if (!name) return;
66+
if (!isPascalCase(name)) {
67+
context.report({
68+
node: node.id,
69+
message: `Type name '${name}' is not PascalCase.`,
70+
fix(fixer) {
71+
return fixer.replaceText(node.id, toPascalCase(name));
72+
},
73+
});
74+
}
75+
},
76+
TSInterfaceDeclaration(node) {
77+
const name = node.id.name;
78+
if (!name) return;
79+
if (!isPascalCase(name)) {
80+
context.report({
81+
node: node.id,
82+
message: `Interface name '${name}' is not PascalCase.`,
83+
fix(fixer) {
84+
return fixer.replaceText(node.id, toPascalCase(name));
85+
},
86+
});
87+
}
88+
},
89+
TSEnumDeclaration(node) {
90+
const name = node.id.name;
91+
if (!name) return;
92+
if (!isPascalCase(name)) {
93+
context.report({
94+
node: node.id,
95+
message: `Enum name '${name}' is not PascalCase.`,
96+
fix(fixer) {
97+
return fixer.replaceText(node.id, toPascalCase(name));
98+
},
99+
});
100+
}
101+
},
102+
FunctionDeclaration(node) {
103+
const id = node.id;
104+
if (!id) return;
105+
const name = id.name;
106+
if (!name) return;
107+
if (!isCamelCase(name)) {
108+
context.report({
109+
node: id,
110+
message: `Function name '${name}' is not camelCase.`,
111+
fix(fixer) {
112+
return fixer.replaceText(id, toCamelCase(name));
113+
},
114+
});
115+
}
116+
},
117+
ClassDeclaration(node) {
118+
const id = node.id;
119+
if (!id) return;
120+
const name = id.name;
121+
if (!name) return;
122+
if (!isPascalCase(name)) {
123+
context.report({
124+
node: id,
125+
message: `Class name '${name}' is not PascalCase.`,
126+
fix(fixer) {
127+
return fixer.replaceText(id, toPascalCase(name));
128+
},
129+
});
130+
}
131+
},
132+
MethodDefinition(node) {
133+
const key = node.key;
134+
if (key.type !== "Identifier") return;
135+
const name = key.name;
136+
if (!name) return;
137+
if (!isCamelCase(name)) {
138+
context.report({
139+
node: key,
140+
message: `Method name '${name}' is not camelCase.`,
141+
fix(fixer) {
142+
return fixer.replaceText(key, toCamelCase(name));
143+
},
144+
});
145+
}
146+
},
147+
PropertyDefinition(node) {
148+
const key = node.key;
149+
switch (key.type) {
150+
case "Identifier":
151+
case "PrivateIdentifier": {
152+
const name = key.name;
153+
if (!name) return;
154+
if (!isCamelCase(name)) {
155+
context.report({
156+
node: key,
157+
message: `Property name '${name}' is not camelCase.`,
158+
fix(fixer) {
159+
return fixer.replaceText(key, toCamelCase(name));
160+
},
161+
});
162+
}
163+
break;
164+
}
165+
default:
166+
break;
167+
}
168+
},
169+
VariableDeclaration(node) {
170+
for (const declaration of node.declarations) {
171+
const id = declaration.id;
172+
if (id.type !== "Identifier") return;
173+
const name = id.name;
174+
if (!name) return;
175+
if (
176+
!isConstantCase(name) && !isCamelCase(name) &&
177+
!isPascalCase(name)
178+
) {
179+
context.report({
180+
node: id,
181+
message:
182+
`Variable name '${name}' is not camelCase, PascalCase, or CONSTANT_CASE.`,
183+
});
184+
}
185+
}
186+
},
187+
};
188+
},
189+
},
41190
},
42191
} satisfies Deno.lint.Plugin;

Diff for: _tools/lint_plugin_test.ts

+115
Original file line numberDiff line numberDiff line change
@@ -55,3 +55,118 @@ class MyClass {
5555
}],
5656
);
5757
});
58+
59+
Deno.test("deno-style-guide/naming-convention", {
60+
ignore: !Deno.version.deno.startsWith("2"),
61+
}, () => {
62+
// Good
63+
assertLintPluginDiagnostics(
64+
`
65+
const CONSTANT_NAME = "foo";
66+
const constName = "foo";
67+
let letName = "foo";
68+
var varName = "foo";
69+
70+
// trailing underscore is allowed for avoiding conflicts
71+
const Date_ = Date;
72+
73+
// trailing capital letter is allowed in PascalCase
74+
type FooX = string;
75+
76+
const objectName = {
77+
methodName() {
78+
},
79+
get getName() {
80+
},
81+
set setName(value) {
82+
}
83+
};
84+
function functionName() {
85+
}
86+
class ClassName {}
87+
const ClassName2 = class {};
88+
89+
type TypeName = unknown;
90+
interface InterfaceName {};
91+
enum EnumName {
92+
foo = "bar",
93+
}
94+
95+
`,
96+
[],
97+
);
98+
99+
// Bad
100+
assertLintPluginDiagnostics(
101+
`
102+
const CONSTANT_name = "foo";
103+
104+
function FunctionName() {
105+
}
106+
107+
function fn() {
108+
const NESTED_CONSTANT_CASE = "foo";
109+
}
110+
111+
class className {}
112+
113+
type typeName = unknown;
114+
115+
// capital-only acronym name is not PascalCase
116+
// This should be Db, not DB
117+
type DB = {};
118+
119+
interface interfaceName {};
120+
enum enumName {
121+
foo = "bar",
122+
}
123+
124+
125+
`,
126+
[
127+
{
128+
fix: [],
129+
hint: undefined,
130+
id: "deno-style-guide/naming-convention",
131+
message:
132+
"Variable name 'CONSTANT_name' is not camelCase, PascalCase, or CONSTANT_CASE.",
133+
range: [7, 20],
134+
},
135+
{
136+
fix: [{ range: [40, 52], text: "functionName" }],
137+
hint: undefined,
138+
id: "deno-style-guide/naming-convention",
139+
message: "Function name 'FunctionName' is not camelCase.",
140+
range: [40, 52],
141+
},
142+
{
143+
fix: [{ range: [123, 132], text: "ClassName" }],
144+
hint: undefined,
145+
id: "deno-style-guide/naming-convention",
146+
message: "Class name 'className' is not PascalCase.",
147+
range: [123, 132],
148+
},
149+
{
150+
fix: [{ range: [142, 150], text: "TypeName" }],
151+
hint: undefined,
152+
id: "deno-style-guide/naming-convention",
153+
message: "Type name 'typeName' is not PascalCase.",
154+
range: [142, 150],
155+
},
156+
{
157+
fix: [{ range: [244, 246], text: "Db" }],
158+
hint: undefined,
159+
id: "deno-style-guide/naming-convention",
160+
message: "Type name 'DB' is not PascalCase.",
161+
range: [244, 246],
162+
},
163+
{
164+
fix: [{ range: [264, 277], text: "InterfaceName" }],
165+
hint: undefined,
166+
id: "deno-style-guide/naming-convention",
167+
message: "Interface name 'interfaceName' is not PascalCase.",
168+
range: [264, 277],
169+
},
170+
],
171+
);
172+
});

Diff for: assert/object_match.ts

+10-10
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ export function assertObjectMatch(
4646
);
4747
}
4848

49-
type loose = Record<PropertyKey, unknown>;
49+
type Loose = Record<PropertyKey, unknown>;
5050

5151
function isObject(val: unknown): boolean {
5252
return typeof val === "object" && val !== null;
@@ -61,11 +61,11 @@ function defineProperty(target: object, key: PropertyKey, value: unknown) {
6161
});
6262
}
6363

64-
function filter(a: loose, b: loose): loose {
65-
const seen = new WeakMap<loose | unknown[], loose | unknown[]>();
64+
function filter(a: Loose, b: Loose): Loose {
65+
const seen = new WeakMap<Loose | unknown[], Loose | unknown[]>();
6666
return filterObject(a, b);
6767

68-
function filterObject(a: loose, b: loose): loose {
68+
function filterObject(a: Loose, b: Loose): Loose {
6969
// Prevent infinite loop with circular references with same filter
7070
const memo = seen.get(a);
7171
if (memo && (memo === b)) return a;
@@ -81,7 +81,7 @@ function filter(a: loose, b: loose): loose {
8181
}
8282

8383
// Filter keys and symbols which are present in both actual and expected
84-
const filtered = {} as loose;
84+
const filtered = {} as Loose;
8585
const keysA = Reflect.ownKeys(a);
8686
const keysB = Reflect.ownKeys(b);
8787
const entries = keysA.filter((key) => keysB.includes(key))
@@ -101,7 +101,7 @@ function filter(a: loose, b: loose): loose {
101101
continue;
102102
}
103103

104-
const subset = (b as loose)[key];
104+
const subset = (b as Loose)[key];
105105

106106
// On array references, build a filtered array and filter nested objects inside
107107
if (Array.isArray(value) && Array.isArray(subset)) {
@@ -121,7 +121,7 @@ function filter(a: loose, b: loose): loose {
121121
([k, v]) => {
122122
const v2 = subset.get(k);
123123
if (isObject(v) && isObject(v2)) {
124-
return [k, filterObject(v as loose, v2 as loose)];
124+
return [k, filterObject(v as Loose, v2 as Loose)];
125125
}
126126
return [k, v];
127127
},
@@ -140,7 +140,7 @@ function filter(a: loose, b: loose): loose {
140140
defineProperty(
141141
filtered,
142142
key,
143-
filterObject(value as loose, subset as loose),
143+
filterObject(value as Loose, subset as Loose),
144144
);
145145
continue;
146146
}
@@ -186,7 +186,7 @@ function filter(a: loose, b: loose): loose {
186186
.map(([k, v]) => {
187187
const v2 = subset.get(k);
188188
if (isObject(v) && isObject(v2)) {
189-
return [k, filterObject(v as loose, v2 as loose)];
189+
return [k, filterObject(v as Loose, v2 as Loose)];
190190
}
191191

192192
return [k, v];
@@ -202,7 +202,7 @@ function filter(a: loose, b: loose): loose {
202202
continue;
203203
}
204204

205-
filtered.push(filterObject(value as loose, subset as loose));
205+
filtered.push(filterObject(value as Loose, subset as Loose));
206206
continue;
207207
}
208208

Diff for: assert/object_match_test.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,11 @@ const d = { corge: c, grault: c };
99
const e = { foo: true } as { [key: string]: unknown };
1010
e.bar = e;
1111
const f = { [sym]: true, bar: false };
12-
interface r {
12+
interface R {
1313
foo: boolean;
1414
bar: boolean;
1515
}
16-
const g: r = { foo: true, bar: false };
16+
const g: R = { foo: true, bar: false };
1717
const h = { foo: [1, 2, 3], bar: true };
1818
const i = { foo: [a, e], bar: true };
1919
const j = { foo: [[1, 2, 3]], bar: true };

0 commit comments

Comments
 (0)