@dmitryrechkin/eslint-standard 1.5.7 → 1.5.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +91 -10
- package/eslint.config.mjs +91 -0
- package/package.json +5 -3
- package/src/cli/check-deps.mjs +11 -21
- package/src/cli/format.mjs +22 -0
- package/src/cli/index.mjs +10 -0
- package/src/cli/install-deps.mjs +54 -36
- package/src/cli/lint.mjs +21 -0
- package/src/cli/postinstall-check.mjs +4 -4
- package/src/cli/postinstall.mjs +3 -3
- package/src/plugins/standard-conventions.mjs +1156 -0
- package/src/cli/setup-aggressive-cleanup.mjs +0 -97
|
@@ -0,0 +1,1156 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Rule: Services must have only one public method
|
|
5
|
+
* @type {import('eslint').Rule.RuleModule}
|
|
6
|
+
*/
|
|
7
|
+
const serviceSinglePublicMethodRule = {
|
|
8
|
+
meta: {
|
|
9
|
+
type: 'suggestion',
|
|
10
|
+
docs: {
|
|
11
|
+
description: 'Enforce that services have only one public method',
|
|
12
|
+
category: 'Best Practices',
|
|
13
|
+
recommended: false
|
|
14
|
+
},
|
|
15
|
+
schema: []
|
|
16
|
+
},
|
|
17
|
+
create(context) {
|
|
18
|
+
return {
|
|
19
|
+
ClassDeclaration(node) {
|
|
20
|
+
if (!node.id || !node.id.name.endsWith('Service')) {
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const publicMethods = node.body.body.filter(member => {
|
|
25
|
+
return (
|
|
26
|
+
member.type === 'MethodDefinition' &&
|
|
27
|
+
member.kind === 'method' &&
|
|
28
|
+
(member.accessibility === 'public' || !member.accessibility) &&
|
|
29
|
+
!member.static
|
|
30
|
+
);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
if (publicMethods.length > 1) {
|
|
34
|
+
context.report({
|
|
35
|
+
node: node.id,
|
|
36
|
+
message: 'Service {{ name }} has {{ count }} public methods. Services should have only one public method.',
|
|
37
|
+
data: {
|
|
38
|
+
name: node.id.name,
|
|
39
|
+
count: publicMethods.length
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
/**
|
|
50
|
+
* Rule: Factories must have only one public method
|
|
51
|
+
* @type {import('eslint').Rule.RuleModule}
|
|
52
|
+
*/
|
|
53
|
+
const factorySinglePublicMethodRule = {
|
|
54
|
+
meta: {
|
|
55
|
+
type: 'suggestion',
|
|
56
|
+
docs: {
|
|
57
|
+
description: 'Enforce that factories have only one public method',
|
|
58
|
+
category: 'Best Practices',
|
|
59
|
+
recommended: false
|
|
60
|
+
},
|
|
61
|
+
schema: []
|
|
62
|
+
},
|
|
63
|
+
create(context) {
|
|
64
|
+
return {
|
|
65
|
+
ClassDeclaration(node) {
|
|
66
|
+
if (!node.id || !node.id.name.endsWith('Factory')) {
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const publicMethods = node.body.body.filter(member => {
|
|
71
|
+
return (
|
|
72
|
+
member.type === 'MethodDefinition' &&
|
|
73
|
+
member.kind === 'method' &&
|
|
74
|
+
(member.accessibility === 'public' || !member.accessibility) &&
|
|
75
|
+
!member.static
|
|
76
|
+
);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
if (publicMethods.length > 1) {
|
|
80
|
+
context.report({
|
|
81
|
+
node: node.id,
|
|
82
|
+
message: 'Factory {{ name }} has {{ count }} public methods. Factories should have only one public method.',
|
|
83
|
+
data: {
|
|
84
|
+
name: node.id.name,
|
|
85
|
+
count: publicMethods.length
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Rule: Top-level function name must match filename
|
|
97
|
+
* @type {import('eslint').Rule.RuleModule}
|
|
98
|
+
*/
|
|
99
|
+
const functionNameMatchFilenameRule = {
|
|
100
|
+
meta: {
|
|
101
|
+
type: 'suggestion',
|
|
102
|
+
docs: {
|
|
103
|
+
description: 'Enforce that top-level function name matches filename',
|
|
104
|
+
category: 'Best Practices',
|
|
105
|
+
recommended: false
|
|
106
|
+
},
|
|
107
|
+
schema: []
|
|
108
|
+
},
|
|
109
|
+
create(context) {
|
|
110
|
+
const filename = path.basename(context.getFilename(), path.extname(context.getFilename()));
|
|
111
|
+
|
|
112
|
+
return {
|
|
113
|
+
FunctionDeclaration(node) {
|
|
114
|
+
// Only check top-level functions or functions inside exports
|
|
115
|
+
const isTopLevel = node.parent.type === 'Program' ||
|
|
116
|
+
(node.parent.type === 'ExportNamedDeclaration' && node.parent.parent.type === 'Program') ||
|
|
117
|
+
(node.parent.type === 'ExportDefaultDeclaration' && node.parent.parent.type === 'Program');
|
|
118
|
+
|
|
119
|
+
if (!isTopLevel) {
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (node.id && node.id.name !== filename) {
|
|
124
|
+
// Special case: ignore if filename is 'index'
|
|
125
|
+
if (filename === 'index') {
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
context.report({
|
|
130
|
+
node: node.id,
|
|
131
|
+
message: 'Function name "{{ name }}" does not match filename "{{ filename }}".',
|
|
132
|
+
data: {
|
|
133
|
+
name: node.id.name,
|
|
134
|
+
filename: filename
|
|
135
|
+
}
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Rule: Folder names must be camelCase
|
|
145
|
+
* @type {import('eslint').Rule.RuleModule}
|
|
146
|
+
*/
|
|
147
|
+
const folderCamelCaseRule = {
|
|
148
|
+
meta: {
|
|
149
|
+
type: 'suggestion',
|
|
150
|
+
docs: {
|
|
151
|
+
description: 'Enforce that folder names are camelCase',
|
|
152
|
+
category: 'Best Practices',
|
|
153
|
+
recommended: false
|
|
154
|
+
},
|
|
155
|
+
schema: []
|
|
156
|
+
},
|
|
157
|
+
create(context) {
|
|
158
|
+
return {
|
|
159
|
+
Program() {
|
|
160
|
+
const fullPath = context.getFilename();
|
|
161
|
+
|
|
162
|
+
if (fullPath === '<input>' || fullPath === '<text>') {
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const dirPath = path.dirname(fullPath);
|
|
167
|
+
const relativePath = path.relative(process.cwd(), dirPath);
|
|
168
|
+
|
|
169
|
+
if (!relativePath || relativePath === '.') {
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const folders = relativePath.split(path.sep);
|
|
174
|
+
const camelCaseRegex = /^[a-z][a-zA-Z0-9]*$/;
|
|
175
|
+
|
|
176
|
+
for (const folder of folders) {
|
|
177
|
+
// Skip node_modules, dist, tests, and hidden folders
|
|
178
|
+
if (folder === 'node_modules' || folder === 'dist' || folder === 'tests' || folder.startsWith('.')) {
|
|
179
|
+
continue;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Skip common top-level folders that might not be camelCase (optional, but good for compatibility)
|
|
183
|
+
if (['src', 'apps', 'packages', 'tools', 'docs', 'config'].includes(folder)) {
|
|
184
|
+
continue;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
if (!camelCaseRegex.test(folder)) {
|
|
188
|
+
context.report({
|
|
189
|
+
loc: { line: 1, column: 0 },
|
|
190
|
+
message: 'Folder name "{{ folder }}" should be camelCase.',
|
|
191
|
+
data: { folder }
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Rule: Helpers must have only static methods
|
|
202
|
+
* @type {import('eslint').Rule.RuleModule}
|
|
203
|
+
*/
|
|
204
|
+
const helperStaticOnlyRule = {
|
|
205
|
+
meta: {
|
|
206
|
+
type: 'problem',
|
|
207
|
+
docs: {
|
|
208
|
+
description: 'Enforce that Helper classes only contain static methods',
|
|
209
|
+
category: 'Best Practices',
|
|
210
|
+
recommended: false
|
|
211
|
+
},
|
|
212
|
+
schema: []
|
|
213
|
+
},
|
|
214
|
+
create(context) {
|
|
215
|
+
return {
|
|
216
|
+
ClassDeclaration(node) {
|
|
217
|
+
if (!node.id || !node.id.name.endsWith('Helper')) {
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
const nonStaticMembers = node.body.body.filter(member => {
|
|
222
|
+
// Skip constructors
|
|
223
|
+
if (member.type === 'MethodDefinition' && member.kind === 'constructor') {
|
|
224
|
+
return false;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// Check for non-static methods and properties
|
|
228
|
+
return (
|
|
229
|
+
(member.type === 'MethodDefinition' || member.type === 'PropertyDefinition') &&
|
|
230
|
+
!member.static
|
|
231
|
+
);
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
if (nonStaticMembers.length > 0) {
|
|
235
|
+
for (const member of nonStaticMembers) {
|
|
236
|
+
const memberName = member.key?.name || 'unknown';
|
|
237
|
+
|
|
238
|
+
context.report({
|
|
239
|
+
node: member,
|
|
240
|
+
message: 'Helper class "{{ className }}" should only have static members. Member "{{ memberName }}" is not static.',
|
|
241
|
+
data: {
|
|
242
|
+
className: node.id.name,
|
|
243
|
+
memberName
|
|
244
|
+
}
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
};
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Rule: Non-helper classes should not have static methods (except factories)
|
|
255
|
+
* @type {import('eslint').Rule.RuleModule}
|
|
256
|
+
*/
|
|
257
|
+
const noStaticInNonHelpersRule = {
|
|
258
|
+
meta: {
|
|
259
|
+
type: 'suggestion',
|
|
260
|
+
docs: {
|
|
261
|
+
description: 'Enforce that non-Helper/Factory classes do not have static methods',
|
|
262
|
+
category: 'Best Practices',
|
|
263
|
+
recommended: false
|
|
264
|
+
},
|
|
265
|
+
schema: []
|
|
266
|
+
},
|
|
267
|
+
create(context) {
|
|
268
|
+
return {
|
|
269
|
+
ClassDeclaration(node) {
|
|
270
|
+
if (!node.id) {
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
const className = node.id.name;
|
|
275
|
+
|
|
276
|
+
// Allow static methods in Helpers, Factories, and Registries
|
|
277
|
+
if (className.endsWith('Helper') || className.endsWith('Factory') || className.endsWith('Registry')) {
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
const staticMembers = node.body.body.filter(member => {
|
|
282
|
+
return (
|
|
283
|
+
(member.type === 'MethodDefinition' || member.type === 'PropertyDefinition') &&
|
|
284
|
+
member.static
|
|
285
|
+
);
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
if (staticMembers.length > 0) {
|
|
289
|
+
for (const member of staticMembers) {
|
|
290
|
+
const memberName = member.key?.name || 'unknown';
|
|
291
|
+
|
|
292
|
+
context.report({
|
|
293
|
+
node: member,
|
|
294
|
+
message: 'Class "{{ className }}" should not have static members. Use a Helper class for static methods. Static member: "{{ memberName }}".',
|
|
295
|
+
data: {
|
|
296
|
+
className,
|
|
297
|
+
memberName
|
|
298
|
+
}
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
};
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* Rule: Classes must be in appropriate folders based on their suffix
|
|
309
|
+
* @type {import('eslint').Rule.RuleModule}
|
|
310
|
+
*/
|
|
311
|
+
const classLocationRule = {
|
|
312
|
+
meta: {
|
|
313
|
+
type: 'suggestion',
|
|
314
|
+
docs: {
|
|
315
|
+
description: 'Enforce that classes are located in appropriate folders based on their suffix',
|
|
316
|
+
category: 'Best Practices',
|
|
317
|
+
recommended: false
|
|
318
|
+
},
|
|
319
|
+
schema: [
|
|
320
|
+
{
|
|
321
|
+
type: 'object',
|
|
322
|
+
properties: {
|
|
323
|
+
mappings: {
|
|
324
|
+
type: 'object',
|
|
325
|
+
additionalProperties: {
|
|
326
|
+
type: 'string'
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
},
|
|
330
|
+
additionalProperties: false
|
|
331
|
+
}
|
|
332
|
+
]
|
|
333
|
+
},
|
|
334
|
+
create(context) {
|
|
335
|
+
const options = context.options[0] || {};
|
|
336
|
+
const defaultMappings = {
|
|
337
|
+
Service: 'services',
|
|
338
|
+
Repository: 'repositories',
|
|
339
|
+
Helper: 'helpers',
|
|
340
|
+
Factory: 'factories',
|
|
341
|
+
Transformer: 'transformers',
|
|
342
|
+
Registry: 'registries',
|
|
343
|
+
Adapter: 'adapters'
|
|
344
|
+
};
|
|
345
|
+
const mappings = { ...defaultMappings, ...options.mappings };
|
|
346
|
+
|
|
347
|
+
return {
|
|
348
|
+
ClassDeclaration(node) {
|
|
349
|
+
if (!node.id) {
|
|
350
|
+
return;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
const className = node.id.name;
|
|
354
|
+
const fullPath = context.getFilename();
|
|
355
|
+
|
|
356
|
+
if (fullPath === '<input>' || fullPath === '<text>') {
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
const dirPath = path.dirname(fullPath);
|
|
361
|
+
|
|
362
|
+
for (const [suffix, expectedFolder] of Object.entries(mappings)) {
|
|
363
|
+
if (className.endsWith(suffix)) {
|
|
364
|
+
// Check if the file is in the expected folder or a subfolder
|
|
365
|
+
const pathParts = dirPath.split(path.sep);
|
|
366
|
+
|
|
367
|
+
if (!pathParts.includes(expectedFolder)) {
|
|
368
|
+
context.report({
|
|
369
|
+
node: node.id,
|
|
370
|
+
message: 'Class "{{ className }}" with suffix "{{ suffix }}" should be in a "{{ expectedFolder }}" folder.',
|
|
371
|
+
data: {
|
|
372
|
+
className,
|
|
373
|
+
suffix,
|
|
374
|
+
expectedFolder
|
|
375
|
+
}
|
|
376
|
+
});
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
break; // Only check first matching suffix
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
};
|
|
384
|
+
}
|
|
385
|
+
};
|
|
386
|
+
|
|
387
|
+
/**
|
|
388
|
+
* Rule: Interface files with TypeXXX must be in types folder
|
|
389
|
+
* @type {import('eslint').Rule.RuleModule}
|
|
390
|
+
*/
|
|
391
|
+
const typeLocationRule = {
|
|
392
|
+
meta: {
|
|
393
|
+
type: 'suggestion',
|
|
394
|
+
docs: {
|
|
395
|
+
description: 'Enforce that Type interfaces are located in types folder',
|
|
396
|
+
category: 'Best Practices',
|
|
397
|
+
recommended: false
|
|
398
|
+
},
|
|
399
|
+
schema: []
|
|
400
|
+
},
|
|
401
|
+
create(context) {
|
|
402
|
+
return {
|
|
403
|
+
TSInterfaceDeclaration(node) {
|
|
404
|
+
if (!node.id) {
|
|
405
|
+
return;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
const interfaceName = node.id.name;
|
|
409
|
+
|
|
410
|
+
// Check if it's a Type interface (starts with Type)
|
|
411
|
+
if (!interfaceName.startsWith('Type')) {
|
|
412
|
+
return;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
const fullPath = context.getFilename();
|
|
416
|
+
|
|
417
|
+
if (fullPath === '<input>' || fullPath === '<text>') {
|
|
418
|
+
return;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
const dirPath = path.dirname(fullPath);
|
|
422
|
+
const pathParts = dirPath.split(path.sep);
|
|
423
|
+
|
|
424
|
+
if (!pathParts.includes('types')) {
|
|
425
|
+
context.report({
|
|
426
|
+
node: node.id,
|
|
427
|
+
message: 'Type interface "{{ interfaceName }}" should be in a "types" folder.',
|
|
428
|
+
data: { interfaceName }
|
|
429
|
+
});
|
|
430
|
+
}
|
|
431
|
+
},
|
|
432
|
+
TSTypeAliasDeclaration(node) {
|
|
433
|
+
if (!node.id) {
|
|
434
|
+
return;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
const typeName = node.id.name;
|
|
438
|
+
|
|
439
|
+
// Check if it's a Type alias (starts with Type)
|
|
440
|
+
if (!typeName.startsWith('Type')) {
|
|
441
|
+
return;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
const fullPath = context.getFilename();
|
|
445
|
+
|
|
446
|
+
if (fullPath === '<input>' || fullPath === '<text>') {
|
|
447
|
+
return;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
const dirPath = path.dirname(fullPath);
|
|
451
|
+
const pathParts = dirPath.split(path.sep);
|
|
452
|
+
|
|
453
|
+
if (!pathParts.includes('types')) {
|
|
454
|
+
context.report({
|
|
455
|
+
node: node.id,
|
|
456
|
+
message: 'Type alias "{{ typeName }}" should be in a "types" folder.',
|
|
457
|
+
data: { typeName }
|
|
458
|
+
});
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
};
|
|
462
|
+
}
|
|
463
|
+
};
|
|
464
|
+
|
|
465
|
+
/**
|
|
466
|
+
* Rule: Transformers must have single public method
|
|
467
|
+
* @type {import('eslint').Rule.RuleModule}
|
|
468
|
+
*/
|
|
469
|
+
const transformerSinglePublicMethodRule = {
|
|
470
|
+
meta: {
|
|
471
|
+
type: 'suggestion',
|
|
472
|
+
docs: {
|
|
473
|
+
description: 'Enforce that transformers have only one public method',
|
|
474
|
+
category: 'Best Practices',
|
|
475
|
+
recommended: false
|
|
476
|
+
},
|
|
477
|
+
schema: []
|
|
478
|
+
},
|
|
479
|
+
create(context) {
|
|
480
|
+
return {
|
|
481
|
+
ClassDeclaration(node) {
|
|
482
|
+
if (!node.id || !node.id.name.endsWith('Transformer')) {
|
|
483
|
+
return;
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
const publicMethods = node.body.body.filter(member => {
|
|
487
|
+
return (
|
|
488
|
+
member.type === 'MethodDefinition' &&
|
|
489
|
+
member.kind === 'method' &&
|
|
490
|
+
(member.accessibility === 'public' || !member.accessibility) &&
|
|
491
|
+
!member.static
|
|
492
|
+
);
|
|
493
|
+
});
|
|
494
|
+
|
|
495
|
+
if (publicMethods.length > 1) {
|
|
496
|
+
context.report({
|
|
497
|
+
node: node.id,
|
|
498
|
+
message: 'Transformer {{ name }} has {{ count }} public methods. Transformers should have only one public method.',
|
|
499
|
+
data: {
|
|
500
|
+
name: node.id.name,
|
|
501
|
+
count: publicMethods.length
|
|
502
|
+
}
|
|
503
|
+
});
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
};
|
|
507
|
+
}
|
|
508
|
+
};
|
|
509
|
+
|
|
510
|
+
/**
|
|
511
|
+
* Rule: Only one class per file
|
|
512
|
+
* @type {import('eslint').Rule.RuleModule}
|
|
513
|
+
*/
|
|
514
|
+
const oneClassPerFileRule = {
|
|
515
|
+
meta: {
|
|
516
|
+
type: 'suggestion',
|
|
517
|
+
docs: {
|
|
518
|
+
description: 'Enforce that each file contains only one class',
|
|
519
|
+
category: 'Best Practices',
|
|
520
|
+
recommended: false
|
|
521
|
+
},
|
|
522
|
+
schema: []
|
|
523
|
+
},
|
|
524
|
+
create(context) {
|
|
525
|
+
const classes = [];
|
|
526
|
+
|
|
527
|
+
return {
|
|
528
|
+
ClassDeclaration(node) {
|
|
529
|
+
if (node.id) {
|
|
530
|
+
classes.push(node);
|
|
531
|
+
}
|
|
532
|
+
},
|
|
533
|
+
'Program:exit'() {
|
|
534
|
+
if (classes.length > 1) {
|
|
535
|
+
// Report on all classes except the first one
|
|
536
|
+
for (let idx = 1; idx < classes.length; idx++) {
|
|
537
|
+
context.report({
|
|
538
|
+
node: classes[idx].id,
|
|
539
|
+
message: 'File contains multiple classes. Each class should be in its own file. Found {{ count }} classes.',
|
|
540
|
+
data: {
|
|
541
|
+
count: classes.length
|
|
542
|
+
}
|
|
543
|
+
});
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
};
|
|
548
|
+
}
|
|
549
|
+
};
|
|
550
|
+
|
|
551
|
+
/**
|
|
552
|
+
* Rule: Repository CQRS method naming
|
|
553
|
+
* @type {import('eslint').Rule.RuleModule}
|
|
554
|
+
*/
|
|
555
|
+
const repositoryCqrsRule = {
|
|
556
|
+
meta: {
|
|
557
|
+
type: 'suggestion',
|
|
558
|
+
docs: {
|
|
559
|
+
description: 'Enforce CQRS naming for repository classes (CommandRepository vs QueryRepository)',
|
|
560
|
+
category: 'Best Practices',
|
|
561
|
+
recommended: false
|
|
562
|
+
},
|
|
563
|
+
schema: []
|
|
564
|
+
},
|
|
565
|
+
create(context) {
|
|
566
|
+
const commandMethods = ['create', 'update', 'delete', 'save', 'insert', 'remove', 'add', 'set'];
|
|
567
|
+
const queryMethods = ['get', 'find', 'fetch', 'list', 'search', 'query', 'read', 'load', 'retrieve'];
|
|
568
|
+
|
|
569
|
+
return {
|
|
570
|
+
ClassDeclaration(node) {
|
|
571
|
+
if (!node.id) {
|
|
572
|
+
return;
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
const className = node.id.name;
|
|
576
|
+
|
|
577
|
+
// Only check Repository classes
|
|
578
|
+
if (!className.endsWith('Repository')) {
|
|
579
|
+
return;
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
const isCommandRepository = className.includes('Command');
|
|
583
|
+
const isQueryRepository = className.includes('Query');
|
|
584
|
+
|
|
585
|
+
// If not explicitly typed, skip this check (backward compatibility)
|
|
586
|
+
if (!isCommandRepository && !isQueryRepository) {
|
|
587
|
+
return;
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
const publicMethods = node.body.body.filter(member => {
|
|
591
|
+
return (
|
|
592
|
+
member.type === 'MethodDefinition' &&
|
|
593
|
+
member.kind === 'method' &&
|
|
594
|
+
(member.accessibility === 'public' || !member.accessibility) &&
|
|
595
|
+
!member.static &&
|
|
596
|
+
member.key?.name
|
|
597
|
+
);
|
|
598
|
+
});
|
|
599
|
+
|
|
600
|
+
for (const method of publicMethods) {
|
|
601
|
+
const methodName = method.key.name.toLowerCase();
|
|
602
|
+
|
|
603
|
+
if (isCommandRepository) {
|
|
604
|
+
// Command repositories should not have query methods
|
|
605
|
+
const hasQueryMethod = queryMethods.some(queryMethod => methodName.startsWith(queryMethod));
|
|
606
|
+
|
|
607
|
+
if (hasQueryMethod) {
|
|
608
|
+
context.report({
|
|
609
|
+
node: method.key,
|
|
610
|
+
message: 'CommandRepository "{{ className }}" should not have query method "{{ methodName }}". Use a QueryRepository for read operations.',
|
|
611
|
+
data: {
|
|
612
|
+
className,
|
|
613
|
+
methodName: method.key.name
|
|
614
|
+
}
|
|
615
|
+
});
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
else if (isQueryRepository) {
|
|
619
|
+
// Query repositories should not have command methods
|
|
620
|
+
const hasCommandMethod = commandMethods.some(cmdMethod => methodName.startsWith(cmdMethod));
|
|
621
|
+
|
|
622
|
+
if (hasCommandMethod) {
|
|
623
|
+
context.report({
|
|
624
|
+
node: method.key,
|
|
625
|
+
message: 'QueryRepository "{{ className }}" should not have command method "{{ methodName }}". Use a CommandRepository for write operations.',
|
|
626
|
+
data: {
|
|
627
|
+
className,
|
|
628
|
+
methodName: method.key.name
|
|
629
|
+
}
|
|
630
|
+
});
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
};
|
|
636
|
+
}
|
|
637
|
+
};
|
|
638
|
+
|
|
639
|
+
/**
|
|
640
|
+
* Rule: No Zod schemas in files with classes
|
|
641
|
+
* @type {import('eslint').Rule.RuleModule}
|
|
642
|
+
*/
|
|
643
|
+
const noSchemasInClassFilesRule = {
|
|
644
|
+
meta: {
|
|
645
|
+
type: 'suggestion',
|
|
646
|
+
docs: {
|
|
647
|
+
description: 'Enforce separation of schemas from class files',
|
|
648
|
+
category: 'Best Practices',
|
|
649
|
+
recommended: false
|
|
650
|
+
},
|
|
651
|
+
schema: []
|
|
652
|
+
},
|
|
653
|
+
create(context) {
|
|
654
|
+
let hasClass = false;
|
|
655
|
+
const schemas = [];
|
|
656
|
+
|
|
657
|
+
return {
|
|
658
|
+
ClassDeclaration() {
|
|
659
|
+
hasClass = true;
|
|
660
|
+
},
|
|
661
|
+
VariableDeclarator(node) {
|
|
662
|
+
if (node.id.type === 'Identifier') {
|
|
663
|
+
const name = node.id.name;
|
|
664
|
+
// Check for Schema suffix or z.object/z.string initialization
|
|
665
|
+
const isSchemaName = name.endsWith('Schema');
|
|
666
|
+
let isZodInit = false;
|
|
667
|
+
|
|
668
|
+
if (node.init && node.init.type === 'CallExpression' &&
|
|
669
|
+
node.init.callee.type === 'MemberExpression' &&
|
|
670
|
+
node.init.callee.object.type === 'Identifier' &&
|
|
671
|
+
node.init.callee.object.name === 'z') {
|
|
672
|
+
isZodInit = true;
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
if (isSchemaName || isZodInit) {
|
|
676
|
+
// Check if top-level
|
|
677
|
+
if (node.parent.parent.type === 'Program' ||
|
|
678
|
+
(node.parent.parent.type === 'ExportNamedDeclaration' && node.parent.parent.parent.type === 'Program')) {
|
|
679
|
+
schemas.push(node);
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
},
|
|
684
|
+
'Program:exit'() {
|
|
685
|
+
if (hasClass && schemas.length > 0) {
|
|
686
|
+
schemas.forEach(node => {
|
|
687
|
+
context.report({
|
|
688
|
+
node: node.id,
|
|
689
|
+
message: 'Schema definition "{{ name }}" should not be in a class file. Move it to a separate schemas file or folder.',
|
|
690
|
+
data: {
|
|
691
|
+
name: node.id.name
|
|
692
|
+
}
|
|
693
|
+
});
|
|
694
|
+
});
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
};
|
|
698
|
+
}
|
|
699
|
+
};
|
|
700
|
+
|
|
701
|
+
/**
|
|
702
|
+
* Rule: No types/interfaces in files with classes
|
|
703
|
+
* @type {import('eslint').Rule.RuleModule}
|
|
704
|
+
*/
|
|
705
|
+
const noTypesInClassFilesRule = {
|
|
706
|
+
meta: {
|
|
707
|
+
type: 'suggestion',
|
|
708
|
+
docs: {
|
|
709
|
+
description: 'Enforce separation of types and interfaces from class files',
|
|
710
|
+
category: 'Best Practices',
|
|
711
|
+
recommended: false
|
|
712
|
+
},
|
|
713
|
+
schema: []
|
|
714
|
+
},
|
|
715
|
+
create(context) {
|
|
716
|
+
let hasClass = false;
|
|
717
|
+
const types = [];
|
|
718
|
+
|
|
719
|
+
return {
|
|
720
|
+
ClassDeclaration() {
|
|
721
|
+
hasClass = true;
|
|
722
|
+
},
|
|
723
|
+
TSTypeAliasDeclaration(node) {
|
|
724
|
+
if (node.parent.type === 'Program' ||
|
|
725
|
+
(node.parent.type === 'ExportNamedDeclaration' && node.parent.parent.type === 'Program')) {
|
|
726
|
+
types.push(node);
|
|
727
|
+
}
|
|
728
|
+
},
|
|
729
|
+
TSInterfaceDeclaration(node) {
|
|
730
|
+
if (node.parent.type === 'Program' ||
|
|
731
|
+
(node.parent.type === 'ExportNamedDeclaration' && node.parent.parent.type === 'Program')) {
|
|
732
|
+
types.push(node);
|
|
733
|
+
}
|
|
734
|
+
},
|
|
735
|
+
'Program:exit'() {
|
|
736
|
+
if (hasClass && types.length > 0) {
|
|
737
|
+
types.forEach(node => {
|
|
738
|
+
context.report({
|
|
739
|
+
node: node.id,
|
|
740
|
+
message: 'Type/Interface definition "{{ name }}" should not be in a class file. Move it to a separate types file or folder.',
|
|
741
|
+
data: {
|
|
742
|
+
name: node.id.name
|
|
743
|
+
}
|
|
744
|
+
});
|
|
745
|
+
});
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
};
|
|
749
|
+
}
|
|
750
|
+
};
|
|
751
|
+
|
|
752
|
+
/**
|
|
753
|
+
* Rule: No top-level constants in files with classes
|
|
754
|
+
* @type {import('eslint').Rule.RuleModule}
|
|
755
|
+
*/
|
|
756
|
+
const noConstantsInClassFilesRule = {
|
|
757
|
+
meta: {
|
|
758
|
+
type: 'suggestion',
|
|
759
|
+
docs: {
|
|
760
|
+
description: 'Enforce separation of constants from class files',
|
|
761
|
+
category: 'Best Practices',
|
|
762
|
+
recommended: false
|
|
763
|
+
},
|
|
764
|
+
schema: []
|
|
765
|
+
},
|
|
766
|
+
create(context) {
|
|
767
|
+
let hasClass = false;
|
|
768
|
+
const constants = [];
|
|
769
|
+
|
|
770
|
+
return {
|
|
771
|
+
ClassDeclaration() {
|
|
772
|
+
hasClass = true;
|
|
773
|
+
},
|
|
774
|
+
VariableDeclaration(node) {
|
|
775
|
+
// Check if top-level const
|
|
776
|
+
if (node.kind === 'const' &&
|
|
777
|
+
(node.parent.type === 'Program' ||
|
|
778
|
+
(node.parent.type === 'ExportNamedDeclaration' && node.parent.parent.type === 'Program'))) {
|
|
779
|
+
|
|
780
|
+
node.declarations.forEach(decl => {
|
|
781
|
+
// Skip requires
|
|
782
|
+
if (decl.init && decl.init.type === 'CallExpression' && decl.init.callee.name === 'require') {
|
|
783
|
+
return;
|
|
784
|
+
}
|
|
785
|
+
// Skip Zod schemas (covered by other rule)
|
|
786
|
+
if (decl.id.name && decl.id.name.endsWith('Schema')) {
|
|
787
|
+
return;
|
|
788
|
+
}
|
|
789
|
+
if (decl.init && decl.init.type === 'CallExpression' &&
|
|
790
|
+
decl.init.callee.type === 'MemberExpression' &&
|
|
791
|
+
decl.init.callee.object.name === 'z') {
|
|
792
|
+
return;
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
constants.push(decl);
|
|
796
|
+
});
|
|
797
|
+
}
|
|
798
|
+
},
|
|
799
|
+
'Program:exit'() {
|
|
800
|
+
if (hasClass && constants.length > 0) {
|
|
801
|
+
constants.forEach(node => {
|
|
802
|
+
context.report({
|
|
803
|
+
node: node.id,
|
|
804
|
+
message: 'Constant "{{ name }}" should not be in a class file. Move it to a separate constants file or use a static readonly class property.',
|
|
805
|
+
data: {
|
|
806
|
+
name: node.id.name
|
|
807
|
+
}
|
|
808
|
+
});
|
|
809
|
+
});
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
};
|
|
813
|
+
}
|
|
814
|
+
};
|
|
815
|
+
|
|
816
|
+
/**
|
|
817
|
+
* Rule: Interfaces must start with Type or end with Interface
|
|
818
|
+
* @type {import('eslint').Rule.RuleModule}
|
|
819
|
+
*/
|
|
820
|
+
const interfaceNamingRule = {
|
|
821
|
+
meta: {
|
|
822
|
+
type: 'suggestion',
|
|
823
|
+
docs: {
|
|
824
|
+
description: 'Enforce that interface names start with Type or end with Interface',
|
|
825
|
+
category: 'Naming Conventions',
|
|
826
|
+
recommended: false
|
|
827
|
+
},
|
|
828
|
+
schema: []
|
|
829
|
+
},
|
|
830
|
+
create(context) {
|
|
831
|
+
return {
|
|
832
|
+
TSInterfaceDeclaration(node) {
|
|
833
|
+
if (!node.id) return;
|
|
834
|
+
const name = node.id.name;
|
|
835
|
+
if (!name.startsWith('Type') && !name.endsWith('Interface')) {
|
|
836
|
+
context.report({
|
|
837
|
+
node: node.id,
|
|
838
|
+
message: 'Interface "{{ name }}" should start with "Type" (for data) or end with "Interface" (for contracts).',
|
|
839
|
+
data: { name }
|
|
840
|
+
});
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
};
|
|
844
|
+
}
|
|
845
|
+
};
|
|
846
|
+
|
|
847
|
+
/**
|
|
848
|
+
* Rule: Functions must have explicit return types
|
|
849
|
+
* @type {import('eslint').Rule.RuleModule}
|
|
850
|
+
*/
|
|
851
|
+
const explicitReturnTypeRule = {
|
|
852
|
+
meta: {
|
|
853
|
+
type: 'suggestion',
|
|
854
|
+
docs: {
|
|
855
|
+
description: 'Enforce explicit return types for functions',
|
|
856
|
+
category: 'Type Safety',
|
|
857
|
+
recommended: false
|
|
858
|
+
},
|
|
859
|
+
schema: []
|
|
860
|
+
},
|
|
861
|
+
create(context) {
|
|
862
|
+
function checkFunction(node) {
|
|
863
|
+
if (!node.returnType) {
|
|
864
|
+
const name = node.id ? node.id.name : (node.key ? node.key.name : 'anonymous');
|
|
865
|
+
context.report({
|
|
866
|
+
node: node.id || node.key || node,
|
|
867
|
+
message: 'Function/Method "{{ name }}" is missing an explicit return type.',
|
|
868
|
+
data: { name }
|
|
869
|
+
});
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
return {
|
|
874
|
+
FunctionDeclaration: checkFunction,
|
|
875
|
+
MethodDefinition: (node) => {
|
|
876
|
+
if (node.kind === 'constructor' || node.kind === 'set') return;
|
|
877
|
+
checkFunction(node.value);
|
|
878
|
+
},
|
|
879
|
+
ArrowFunctionExpression: checkFunction,
|
|
880
|
+
FunctionExpression: (node) => {
|
|
881
|
+
if (node.parent.type === 'MethodDefinition') return;
|
|
882
|
+
checkFunction(node);
|
|
883
|
+
}
|
|
884
|
+
};
|
|
885
|
+
}
|
|
886
|
+
};
|
|
887
|
+
|
|
888
|
+
/**
|
|
889
|
+
* Rule: No direct instantiation of classes inside other classes (dependency injection)
|
|
890
|
+
* @type {import('eslint').Rule.RuleModule}
|
|
891
|
+
*/
|
|
892
|
+
const noDirectInstantiationRule = {
|
|
893
|
+
meta: {
|
|
894
|
+
type: 'suggestion',
|
|
895
|
+
docs: {
|
|
896
|
+
description: 'Enforce dependency injection by banning direct instantiation',
|
|
897
|
+
category: 'Best Practices',
|
|
898
|
+
recommended: false
|
|
899
|
+
},
|
|
900
|
+
schema: []
|
|
901
|
+
},
|
|
902
|
+
create(context) {
|
|
903
|
+
const allowedClasses = new Set([
|
|
904
|
+
'Date', 'Error', 'Promise', 'Map', 'Set', 'WeakMap', 'WeakSet',
|
|
905
|
+
'RegExp', 'URL', 'URLSearchParams', 'Buffer', 'Array', 'Object', 'String', 'Number', 'Boolean'
|
|
906
|
+
]);
|
|
907
|
+
|
|
908
|
+
let inClass = false;
|
|
909
|
+
let currentClassName = '';
|
|
910
|
+
|
|
911
|
+
return {
|
|
912
|
+
ClassDeclaration(node) {
|
|
913
|
+
inClass = true;
|
|
914
|
+
if (node.id) {
|
|
915
|
+
currentClassName = node.id.name;
|
|
916
|
+
}
|
|
917
|
+
},
|
|
918
|
+
'ClassDeclaration:exit'() {
|
|
919
|
+
inClass = false;
|
|
920
|
+
currentClassName = '';
|
|
921
|
+
},
|
|
922
|
+
NewExpression(node) {
|
|
923
|
+
if (!inClass) return;
|
|
924
|
+
|
|
925
|
+
// Allow factories to instantiate objects
|
|
926
|
+
if (currentClassName.endsWith('Factory')) {
|
|
927
|
+
return;
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
if (node.callee.type === 'Identifier') {
|
|
931
|
+
const className = node.callee.name;
|
|
932
|
+
if (!allowedClasses.has(className)) {
|
|
933
|
+
context.report({
|
|
934
|
+
node: node,
|
|
935
|
+
message: 'Avoid direct instantiation of "{{ name }}". Use dependency injection instead.',
|
|
936
|
+
data: { name: className }
|
|
937
|
+
});
|
|
938
|
+
}
|
|
939
|
+
}
|
|
940
|
+
}
|
|
941
|
+
};
|
|
942
|
+
}
|
|
943
|
+
};
|
|
944
|
+
|
|
945
|
+
/**
|
|
946
|
+
* Rule: Prefer Enums over union types of string literals
|
|
947
|
+
* @type {import('eslint').Rule.RuleModule}
|
|
948
|
+
*/
|
|
949
|
+
const preferEnumsRule = {
|
|
950
|
+
meta: {
|
|
951
|
+
type: 'suggestion',
|
|
952
|
+
docs: {
|
|
953
|
+
description: 'Prefer Enums over union types of string literals',
|
|
954
|
+
category: 'TypeScript',
|
|
955
|
+
recommended: false
|
|
956
|
+
},
|
|
957
|
+
schema: []
|
|
958
|
+
},
|
|
959
|
+
create(context) {
|
|
960
|
+
return {
|
|
961
|
+
TSTypeAliasDeclaration(node) {
|
|
962
|
+
if (node.typeAnnotation.type === 'TSUnionType') {
|
|
963
|
+
const isAllLiterals = node.typeAnnotation.types.every(
|
|
964
|
+
t => t.type === 'TSLiteralType' && (typeof t.literal.value === 'string' || typeof t.literal.value === 'number')
|
|
965
|
+
);
|
|
966
|
+
|
|
967
|
+
if (isAllLiterals && node.typeAnnotation.types.length > 1) {
|
|
968
|
+
context.report({
|
|
969
|
+
node: node.id,
|
|
970
|
+
message: 'Avoid union types for "{{ name }}". Use an Enum instead.',
|
|
971
|
+
data: { name: node.id.name }
|
|
972
|
+
});
|
|
973
|
+
}
|
|
974
|
+
}
|
|
975
|
+
}
|
|
976
|
+
};
|
|
977
|
+
}
|
|
978
|
+
};
|
|
979
|
+
|
|
980
|
+
/**
|
|
981
|
+
* Rule: Schemas must end with Schema or Table
|
|
982
|
+
* @type {import('eslint').Rule.RuleModule}
|
|
983
|
+
*/
|
|
984
|
+
const schemaNamingRule = {
|
|
985
|
+
meta: {
|
|
986
|
+
type: 'suggestion',
|
|
987
|
+
docs: {
|
|
988
|
+
description: 'Enforce that Zod schemas and Drizzle tables end with Schema or Table',
|
|
989
|
+
category: 'Naming Conventions',
|
|
990
|
+
recommended: false
|
|
991
|
+
},
|
|
992
|
+
schema: []
|
|
993
|
+
},
|
|
994
|
+
create(context) {
|
|
995
|
+
return {
|
|
996
|
+
VariableDeclarator(node) {
|
|
997
|
+
if (node.init && node.init.type === 'CallExpression') {
|
|
998
|
+
let isZodOrDrizzle = false;
|
|
999
|
+
|
|
1000
|
+
// Check for z.something()
|
|
1001
|
+
if (node.init.callee.type === 'MemberExpression' &&
|
|
1002
|
+
node.init.callee.object.type === 'Identifier' &&
|
|
1003
|
+
node.init.callee.object.name === 'z') {
|
|
1004
|
+
isZodOrDrizzle = true;
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
// Check for pgTable()
|
|
1008
|
+
if (node.init.callee.type === 'Identifier' && node.init.callee.name === 'pgTable') {
|
|
1009
|
+
isZodOrDrizzle = true;
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
if (isZodOrDrizzle && node.id.type === 'Identifier') {
|
|
1013
|
+
const name = node.id.name;
|
|
1014
|
+
if (!name.endsWith('Schema') && !name.endsWith('Table')) {
|
|
1015
|
+
context.report({
|
|
1016
|
+
node: node.id,
|
|
1017
|
+
message: 'Schema/Table definition "{{ name }}" should end with "Schema" or "Table".',
|
|
1018
|
+
data: { name }
|
|
1019
|
+
});
|
|
1020
|
+
}
|
|
1021
|
+
}
|
|
1022
|
+
}
|
|
1023
|
+
}
|
|
1024
|
+
};
|
|
1025
|
+
}
|
|
1026
|
+
};
|
|
1027
|
+
|
|
1028
|
+
/**
|
|
1029
|
+
* Rule: Repository methods accepting 'id' must end with 'ById'
|
|
1030
|
+
* @type {import('eslint').Rule.RuleModule}
|
|
1031
|
+
*/
|
|
1032
|
+
const repositoryByIdRule = {
|
|
1033
|
+
meta: {
|
|
1034
|
+
type: 'suggestion',
|
|
1035
|
+
docs: {
|
|
1036
|
+
description: 'Enforce "ById" suffix for repository methods that take an "id" parameter',
|
|
1037
|
+
category: 'Naming Conventions',
|
|
1038
|
+
recommended: false
|
|
1039
|
+
},
|
|
1040
|
+
schema: []
|
|
1041
|
+
},
|
|
1042
|
+
create(context) {
|
|
1043
|
+
return {
|
|
1044
|
+
MethodDefinition(node) {
|
|
1045
|
+
// Only check methods in Repository classes
|
|
1046
|
+
const classNode = node.parent.parent;
|
|
1047
|
+
if (!classNode || !classNode.id || !classNode.id.name.endsWith('Repository')) {
|
|
1048
|
+
return;
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
// Skip constructors and static methods
|
|
1052
|
+
if (node.kind === 'constructor' || node.static) {
|
|
1053
|
+
return;
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
const methodParam = node.value.params[0];
|
|
1057
|
+
if (!methodParam) {
|
|
1058
|
+
return;
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1061
|
+
// Check if first parameter is named 'id'
|
|
1062
|
+
let paramName = '';
|
|
1063
|
+
if (methodParam.type === 'Identifier') {
|
|
1064
|
+
paramName = methodParam.name;
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1067
|
+
if (paramName === 'id') {
|
|
1068
|
+
const methodName = node.key.name;
|
|
1069
|
+
if (!methodName.endsWith('ById')) {
|
|
1070
|
+
context.report({
|
|
1071
|
+
node: node.key,
|
|
1072
|
+
message: 'Repository method "{{ methodName }}" accepts "id" parameter but does not end with "ById". Rename to "{{ methodName }}ById".',
|
|
1073
|
+
data: { methodName }
|
|
1074
|
+
});
|
|
1075
|
+
}
|
|
1076
|
+
}
|
|
1077
|
+
}
|
|
1078
|
+
};
|
|
1079
|
+
}
|
|
1080
|
+
};
|
|
1081
|
+
|
|
1082
|
+
/**
|
|
1083
|
+
* Rule: Avoid using 'utils' folder, prefer 'helpers'
|
|
1084
|
+
* @type {import('eslint').Rule.RuleModule}
|
|
1085
|
+
*/
|
|
1086
|
+
const noUtilsFolderRule = {
|
|
1087
|
+
meta: {
|
|
1088
|
+
type: 'suggestion',
|
|
1089
|
+
docs: {
|
|
1090
|
+
description: 'Enforce that "utils" folders are not used, preferring "helpers" instead',
|
|
1091
|
+
category: 'Best Practices',
|
|
1092
|
+
recommended: false
|
|
1093
|
+
},
|
|
1094
|
+
schema: []
|
|
1095
|
+
},
|
|
1096
|
+
create(context) {
|
|
1097
|
+
return {
|
|
1098
|
+
Program(node) {
|
|
1099
|
+
const fullPath = context.getFilename();
|
|
1100
|
+
|
|
1101
|
+
if (fullPath === '<input>' || fullPath === '<text>') {
|
|
1102
|
+
return;
|
|
1103
|
+
}
|
|
1104
|
+
|
|
1105
|
+
const dirPath = path.dirname(fullPath);
|
|
1106
|
+
const relativePath = path.relative(process.cwd(), dirPath);
|
|
1107
|
+
|
|
1108
|
+
if (!relativePath || relativePath === '.') {
|
|
1109
|
+
return;
|
|
1110
|
+
}
|
|
1111
|
+
|
|
1112
|
+
const folders = relativePath.split(path.sep);
|
|
1113
|
+
|
|
1114
|
+
// Check if any folder is named 'utils' or 'util'
|
|
1115
|
+
for (const folder of folders) {
|
|
1116
|
+
if (folder.toLowerCase() === 'utils' || folder.toLowerCase() === 'util') {
|
|
1117
|
+
context.report({
|
|
1118
|
+
node: node,
|
|
1119
|
+
loc: { line: 1, column: 0 },
|
|
1120
|
+
message: 'Avoid using "{{ folder }}" folder. Use "helpers" for shared logic, or specific domain names (e.g. "services", "factories").',
|
|
1121
|
+
data: { folder }
|
|
1122
|
+
});
|
|
1123
|
+
// Report only once per file
|
|
1124
|
+
break;
|
|
1125
|
+
}
|
|
1126
|
+
}
|
|
1127
|
+
}
|
|
1128
|
+
};
|
|
1129
|
+
}
|
|
1130
|
+
};
|
|
1131
|
+
|
|
1132
|
+
export default {
|
|
1133
|
+
rules: {
|
|
1134
|
+
'service-single-public-method': serviceSinglePublicMethodRule,
|
|
1135
|
+
'factory-single-public-method': factorySinglePublicMethodRule,
|
|
1136
|
+
'function-name-match-filename': functionNameMatchFilenameRule,
|
|
1137
|
+
'folder-camel-case': folderCamelCaseRule,
|
|
1138
|
+
'helper-static-only': helperStaticOnlyRule,
|
|
1139
|
+
'no-static-in-non-helpers': noStaticInNonHelpersRule,
|
|
1140
|
+
'class-location': classLocationRule,
|
|
1141
|
+
'type-location': typeLocationRule,
|
|
1142
|
+
'transformer-single-public-method': transformerSinglePublicMethodRule,
|
|
1143
|
+
'one-class-per-file': oneClassPerFileRule,
|
|
1144
|
+
'repository-cqrs': repositoryCqrsRule,
|
|
1145
|
+
'no-schemas-in-class-files': noSchemasInClassFilesRule,
|
|
1146
|
+
'no-types-in-class-files': noTypesInClassFilesRule,
|
|
1147
|
+
'no-constants-in-class-files': noConstantsInClassFilesRule,
|
|
1148
|
+
'interface-naming': interfaceNamingRule,
|
|
1149
|
+
'explicit-return-type': explicitReturnTypeRule,
|
|
1150
|
+
'no-direct-instantiation': noDirectInstantiationRule,
|
|
1151
|
+
'prefer-enums': preferEnumsRule,
|
|
1152
|
+
'schema-naming': schemaNamingRule,
|
|
1153
|
+
'repository-by-id': repositoryByIdRule,
|
|
1154
|
+
'no-utils-folder': noUtilsFolderRule
|
|
1155
|
+
}
|
|
1156
|
+
};
|