@echoes-of-order/eslint-config 1.121.0
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/CHANGELOG.md +1093 -0
- package/configs/.gitkeep +1 -0
- package/configs/admin.js +203 -0
- package/configs/api-client.js +46 -0
- package/configs/backend.js +895 -0
- package/configs/domains.js +123 -0
- package/configs/frontend.js +30 -0
- package/configs/image-server.js +26 -0
- package/configs/ionos-proxy.js +372 -0
- package/configs/nestjs.js +156 -0
- package/configs/node.js +92 -0
- package/configs/react.js +111 -0
- package/configs/wiki.js +42 -0
- package/index.js +39 -0
- package/package.json +85 -0
- package/rules/.gitkeep +1 -0
- package/rules/__tests__/analyze-relation-usage.test.js.disabled +300 -0
- package/rules/__tests__/complexity.test.js.disabled +300 -0
- package/rules/__tests__/enforce-dto-factory-in-services.integration.test.js +226 -0
- package/rules/__tests__/enforce-dto-factory-in-services.test.js +177 -0
- package/rules/__tests__/enforce-entity-dto-create-no-id.integration.test.js +18 -0
- package/rules/__tests__/enforce-function-argument-count.test.js.disabled +300 -0
- package/rules/__tests__/enforce-repository-token-handling.test.js +58 -0
- package/rules/__tests__/english-only-code-strings.test.js.disabled +300 -0
- package/rules/__tests__/eslint-rules.integration.test.ts +350 -0
- package/rules/__tests__/integration-test-controller-response-dto.js +261 -0
- package/rules/__tests__/integration-test-dto-factory-in-services.js +260 -0
- package/rules/__tests__/integration-test-no-entity-type-casting.js +161 -0
- package/rules/__tests__/integration-test-typeorm-naming-conventions.js +501 -0
- package/rules/__tests__/test-config.js +33 -0
- package/rules/admin-controller-security.js +180 -0
- package/rules/analyze-relation-usage.js +687 -0
- package/rules/api-response-dto.js +174 -0
- package/rules/auth-guard-required.js +142 -0
- package/rules/backend-specific.js +36 -0
- package/rules/best-practices.js +421 -0
- package/rules/complexity.js +20 -0
- package/rules/controller-architecture.js +340 -0
- package/rules/controller-naming-conventions.js +190 -0
- package/rules/controller-readonly-restriction.js +148 -0
- package/rules/controller-swagger-complete.js +312 -0
- package/rules/controller-swagger-docs.js +119 -0
- package/rules/controller-swagger-english.js +320 -0
- package/rules/coordinate-naming.js +132 -0
- package/rules/custom-mui-button.js +135 -0
- package/rules/dead-code-detection-backend.js +50 -0
- package/rules/dead-code-detection-frontend.js +48 -0
- package/rules/dead-code-detection.js +71 -0
- package/rules/debug-controller-response-dto.js +79 -0
- package/rules/deprecate.js +8 -0
- package/rules/dto-annotation-property-consistency.js +111 -0
- package/rules/dto-entity-mapping-completeness.js +688 -0
- package/rules/dto-entity-swagger-separation.js +265 -0
- package/rules/dto-entity-type-consistency.js +352 -0
- package/rules/dto-entity-type-matching.js +519 -0
- package/rules/dto-naming-convention.js +98 -0
- package/rules/dto-visibility-modifiers.js +159 -0
- package/rules/enforce-api-versioning.js +122 -0
- package/rules/enforce-app-module-registration.js +179 -0
- package/rules/enforce-basecontroller.js +152 -0
- package/rules/enforce-body-request-dto.js +141 -0
- package/rules/enforce-controller-response-dto.js +349 -0
- package/rules/enforce-custom-error-classes.js +242 -0
- package/rules/enforce-database-transaction-safety.js +179 -0
- package/rules/enforce-dto-constructor.js +95 -0
- package/rules/enforce-dto-create-parameter-types.js +170 -0
- package/rules/enforce-dto-create-pattern.js +274 -0
- package/rules/enforce-dto-entity-creation.js +164 -0
- package/rules/enforce-dto-factory-in-services.js +188 -0
- package/rules/enforce-dto-from-entity-method.js +47 -0
- package/rules/enforce-dto-from-entity.js +314 -0
- package/rules/enforce-dto-naming-conventions.js +212 -0
- package/rules/enforce-dto-naming.js +176 -0
- package/rules/enforce-dto-usage-simple.js +114 -0
- package/rules/enforce-dto-usage.js +407 -0
- package/rules/enforce-eager-translation-loading.js +178 -0
- package/rules/enforce-entity-creation-pattern.js +137 -0
- package/rules/enforce-entity-dto-convert-method.js +157 -0
- package/rules/enforce-entity-dto-create-no-id.js +117 -0
- package/rules/enforce-entity-dto-extends-base.js +141 -0
- package/rules/enforce-entity-dto-from-request-dto-structure.js +113 -0
- package/rules/enforce-entity-dto-fromentity-complex.js +69 -0
- package/rules/enforce-entity-dto-fromentity-simple.js +69 -0
- package/rules/enforce-entity-dto-fromrequestdto-structure.js +262 -0
- package/rules/enforce-entity-dto-methods-restriction.js +159 -0
- package/rules/enforce-entity-dto-no-request-dto.js +102 -0
- package/rules/enforce-entity-dto-optional-auto-fields.js +101 -0
- package/rules/enforce-entity-dto-required-methods.js +248 -0
- package/rules/enforce-entity-factory-pattern.js +180 -0
- package/rules/enforce-entity-instantiation-in-toentity.js +125 -0
- package/rules/enforce-enum-for-playable-entities.js +95 -0
- package/rules/enforce-error-handling.js +257 -0
- package/rules/enforce-explicit-dto-types.js +118 -0
- package/rules/enforce-from-request-dto-usage.js +62 -0
- package/rules/enforce-generic-entity-dto.js +71 -0
- package/rules/enforce-inject-decorator.js +133 -0
- package/rules/enforce-lazy-type-loading.js +170 -0
- package/rules/enforce-module-existence.js +157 -0
- package/rules/enforce-nonentity-dto-create.js +107 -0
- package/rules/enforce-playable-entity-naming.js +108 -0
- package/rules/enforce-repository-token-handling.js +92 -0
- package/rules/enforce-request-dto-no-entity-dto.js +201 -0
- package/rules/enforce-request-dto-required-fields.js +217 -0
- package/rules/enforce-result-pattern.js +45 -0
- package/rules/enforce-service-relation-loading.js +116 -0
- package/rules/enforce-test-coverage.js +96 -0
- package/rules/enforce-toentity-conditional-assignment.js +132 -0
- package/rules/enforce-translations-required.js +203 -0
- package/rules/enforce-typeorm-naming-conventions.js +366 -0
- package/rules/enforce-vite-health-metrics.js +240 -0
- package/rules/entity-required-properties.js +321 -0
- package/rules/entity-to-dto-test.js +73 -0
- package/rules/enum-database-validation.js +149 -0
- package/rules/errors.js +190 -0
- package/rules/es6.js +204 -0
- package/rules/eslint-plugin-no-comments.js +44 -0
- package/rules/filename-class-name-match.js +62 -0
- package/rules/forbid-fromentity-outside-entity-folder.js +237 -0
- package/rules/function-params-newline.js +111 -0
- package/rules/imports.js +264 -0
- package/rules/jest.js +13 -0
- package/rules/jsx.js +16 -0
- package/rules/max-classes-per-file.js +49 -0
- package/rules/multiline-formatting.js +146 -0
- package/rules/no-blank-lines-between-decorators-and-properties.js +95 -0
- package/rules/no-comments.js +62 -0
- package/rules/no-dto-constructors.js +126 -0
- package/rules/no-dto-default-values.js +220 -0
- package/rules/no-dto-duplicates.js +127 -0
- package/rules/no-dto-in-entity.js +99 -0
- package/rules/no-dynamic-import-in-types.js +71 -0
- package/rules/no-dynamic-imports-in-controllers.js +95 -0
- package/rules/no-entity-imports-in-controllers.js +101 -0
- package/rules/no-entity-in-swagger-docs.js +139 -0
- package/rules/no-entity-type-casting.js +104 -0
- package/rules/no-fetch.js +77 -0
- package/rules/no-import-meta-env.js +151 -0
- package/rules/no-inline-styles.js +5 -0
- package/rules/no-magic-values.js +85 -0
- package/rules/no-partial-type.js +168 -0
- package/rules/no-relative-imports.js +31 -0
- package/rules/no-tsyringe.js +181 -0
- package/rules/no-type-assertion.js +175 -0
- package/rules/no-undefined-entity-properties.js +121 -0
- package/rules/node.js +44 -0
- package/rules/perfectionist.js +50 -0
- package/rules/performance-minimal.js +155 -0
- package/rules/performance.js +44 -0
- package/rules/pino-logger-format.js +200 -0
- package/rules/prefer-dto-classes.js +112 -0
- package/rules/prefer-dto-create-method.js +225 -0
- package/rules/promises.js +17 -0
- package/rules/react-hooks.js +15 -0
- package/rules/react.js +28 -0
- package/rules/regexp.js +70 -0
- package/rules/require-dto-response.js +81 -0
- package/rules/require-valid-relations.js +388 -0
- package/rules/result-pattern.js +162 -0
- package/rules/security.js +37 -0
- package/rules/service-architecture.js +148 -0
- package/rules/sonarjs.js +26 -0
- package/rules/strict.js +7 -0
- package/rules/style.js +611 -0
- package/rules/stylistic.js +93 -0
- package/rules/typeorm-column-type-validation.js +224 -0
- package/rules/typescript-advanced.js +113 -0
- package/rules/typescript-core.js +111 -0
- package/rules/typescript.js +146 -0
- package/rules/unicorn.js +168 -0
- package/rules/variables.js +51 -0
- package/rules/websocket-architecture.js +115 -0
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ESLint-Regeln für Controller-Architektur
|
|
3
|
+
* 1. Controller dürfen keine Repositories direkt nutzen, nur Services
|
|
4
|
+
* 2. Rückgabewerte müssen DTOs sein, nie Entities
|
|
5
|
+
* 3. Response-Handling muss über sendSuccess/sendError erfolgen
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/** @type {import('eslint').Rule.RuleModule} */
|
|
9
|
+
const noDirectRepositoryUseRule = {
|
|
10
|
+
meta: {
|
|
11
|
+
type: "problem",
|
|
12
|
+
docs: {
|
|
13
|
+
description: "Controller dürfen keine Repositories direkt nutzen, nur Services",
|
|
14
|
+
category: "Architecture",
|
|
15
|
+
recommended: true,
|
|
16
|
+
},
|
|
17
|
+
schema: [],
|
|
18
|
+
messages: {
|
|
19
|
+
noDirectRepository: "Controller dürfen keine Repositories direkt nutzen. Verwende stattdessen einen Service: '{{usage}}'",
|
|
20
|
+
noDirectDataSource: "Controller dürfen DataSource nicht direkt nutzen. Verwende stattdessen einen Service: '{{usage}}'",
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
create(context) {
|
|
24
|
+
const controllerFiles = /Controller\.ts$/;
|
|
25
|
+
const filename = context.getFilename();
|
|
26
|
+
const isControllerFile = controllerFiles.test(filename) &&
|
|
27
|
+
!/BaseController\.ts$/.test(filename);
|
|
28
|
+
|
|
29
|
+
if (!isControllerFile) return {};
|
|
30
|
+
|
|
31
|
+
return {
|
|
32
|
+
// Prüfe auf direkte Repository-Nutzung
|
|
33
|
+
CallExpression(node) {
|
|
34
|
+
// dataSource.getRepository() oder appDataSource.getRepository()
|
|
35
|
+
if (
|
|
36
|
+
node.callee.type === "MemberExpression" &&
|
|
37
|
+
node.callee.property.type === "Identifier" &&
|
|
38
|
+
node.callee.property.name === "getRepository" &&
|
|
39
|
+
node.callee.object.type === "Identifier" &&
|
|
40
|
+
(node.callee.object.name === "dataSource" ||
|
|
41
|
+
node.callee.object.name === "appDataSource" ||
|
|
42
|
+
node.callee.object.name.includes("DataSource"))
|
|
43
|
+
) {
|
|
44
|
+
context.report({
|
|
45
|
+
node,
|
|
46
|
+
messageId: "noDirectRepository",
|
|
47
|
+
data: {
|
|
48
|
+
usage: `${node.callee.object.name}.getRepository()`,
|
|
49
|
+
},
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Repository-Methoden direkt aufrufen
|
|
54
|
+
if (
|
|
55
|
+
node.callee.type === "MemberExpression" &&
|
|
56
|
+
node.callee.object.type === "CallExpression" &&
|
|
57
|
+
node.callee.object.callee.type === "MemberExpression" &&
|
|
58
|
+
node.callee.object.callee.property.name === "getRepository"
|
|
59
|
+
) {
|
|
60
|
+
context.report({
|
|
61
|
+
node,
|
|
62
|
+
messageId: "noDirectRepository",
|
|
63
|
+
data: {
|
|
64
|
+
usage: "repository method call",
|
|
65
|
+
},
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
},
|
|
69
|
+
|
|
70
|
+
// Prüfe auf direkte DataSource-Imports
|
|
71
|
+
ImportDeclaration(node) {
|
|
72
|
+
if (node.source.value === "typeorm" &&
|
|
73
|
+
node.specifiers.some(spec =>
|
|
74
|
+
spec.type === "ImportSpecifier" &&
|
|
75
|
+
spec.imported.name === "DataSource")) {
|
|
76
|
+
context.report({
|
|
77
|
+
node,
|
|
78
|
+
messageId: "noDirectDataSource",
|
|
79
|
+
data: {
|
|
80
|
+
usage: "DataSource import",
|
|
81
|
+
},
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Prüfe auf appDataSource Import
|
|
86
|
+
if (node.source.value &&
|
|
87
|
+
node.source.value.includes("ormconfig") ||
|
|
88
|
+
node.source.value.includes("data-source")) {
|
|
89
|
+
context.report({
|
|
90
|
+
node,
|
|
91
|
+
messageId: "noDirectDataSource",
|
|
92
|
+
data: {
|
|
93
|
+
usage: "appDataSource import",
|
|
94
|
+
},
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
},
|
|
98
|
+
};
|
|
99
|
+
},
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
/** @type {import('eslint').Rule.RuleModule} */
|
|
103
|
+
const requireDtoResponseRule = {
|
|
104
|
+
meta: {
|
|
105
|
+
type: "problem",
|
|
106
|
+
docs: {
|
|
107
|
+
description: "Controller müssen DTOs zurückgeben, nie Entities",
|
|
108
|
+
category: "Architecture",
|
|
109
|
+
recommended: true,
|
|
110
|
+
},
|
|
111
|
+
schema: [],
|
|
112
|
+
messages: {
|
|
113
|
+
returnEntity: "Controller dürfen keine Entities zurückgeben. Verwende DTOs: '{{entityName}}'",
|
|
114
|
+
serviceReturnsEntity: "Service-Methode '{{methodName}}' gibt wahrscheinlich Entity zurück. Controller müssen DTOs verwenden",
|
|
115
|
+
},
|
|
116
|
+
},
|
|
117
|
+
create(context) {
|
|
118
|
+
const controllerFiles = /Controller\.ts$/;
|
|
119
|
+
const filename = context.getFilename();
|
|
120
|
+
const isControllerFile = controllerFiles.test(filename) &&
|
|
121
|
+
!/BaseController\.ts$/.test(filename);
|
|
122
|
+
|
|
123
|
+
if (!isControllerFile) return {};
|
|
124
|
+
|
|
125
|
+
// Sammle Service-Imports und erkenne Entity-verdächtige Patterns
|
|
126
|
+
const serviceImports = new Set();
|
|
127
|
+
const entityImports = new Set();
|
|
128
|
+
|
|
129
|
+
return {
|
|
130
|
+
// Sammle Imports von Services und Entities
|
|
131
|
+
ImportDeclaration(node) {
|
|
132
|
+
if (node.source.value && typeof node.source.value === "string") {
|
|
133
|
+
const importPath = node.source.value;
|
|
134
|
+
|
|
135
|
+
// Erkenne Entity-Imports (endend mit "Entity" oder aus entity/ Ordnern)
|
|
136
|
+
if (importPath.includes("/entity/") ||
|
|
137
|
+
node.specifiers.some(spec =>
|
|
138
|
+
spec.type === "ImportDefaultSpecifier" &&
|
|
139
|
+
spec.local.name.endsWith("Entity"))) {
|
|
140
|
+
node.specifiers.forEach(spec => {
|
|
141
|
+
if (spec.type === "ImportDefaultSpecifier") {
|
|
142
|
+
entityImports.add(spec.local.name);
|
|
143
|
+
}
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Erkenne Service-Imports
|
|
148
|
+
if (importPath.includes("/service/") ||
|
|
149
|
+
node.specifiers.some(spec =>
|
|
150
|
+
spec.type === "ImportDefaultSpecifier" &&
|
|
151
|
+
spec.local.name.endsWith("Service"))) {
|
|
152
|
+
node.specifiers.forEach(spec => {
|
|
153
|
+
if (spec.type === "ImportDefaultSpecifier") {
|
|
154
|
+
serviceImports.add(spec.local.name);
|
|
155
|
+
}
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
},
|
|
160
|
+
|
|
161
|
+
// Prüfe Return-Statements auf Entity-Rückgaben
|
|
162
|
+
ReturnStatement(node) {
|
|
163
|
+
if (node.argument &&
|
|
164
|
+
node.argument.type === "CallExpression" &&
|
|
165
|
+
node.argument.callee.type === "MemberExpression" &&
|
|
166
|
+
node.argument.callee.object.type === "Identifier" &&
|
|
167
|
+
node.argument.callee.object.name === "repo") {
|
|
168
|
+
context.report({
|
|
169
|
+
node,
|
|
170
|
+
messageId: "returnEntity",
|
|
171
|
+
data: {
|
|
172
|
+
entityName: "repository result",
|
|
173
|
+
},
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
},
|
|
177
|
+
|
|
178
|
+
// Prüfe Service-Aufrufe in handleGetAll/handleGetById
|
|
179
|
+
CallExpression(node) {
|
|
180
|
+
// Prüfe auf this.handleGetAll(..., () => this.serviceMethod(), ...)
|
|
181
|
+
if (node.callee.type === "MemberExpression" &&
|
|
182
|
+
node.callee.object.type === "ThisExpression" &&
|
|
183
|
+
(node.callee.property.name === "handleGetAll" ||
|
|
184
|
+
node.callee.property.name === "handleGetById")) {
|
|
185
|
+
|
|
186
|
+
// Finde das Service-Callback (zweites Argument bei handleGetAll)
|
|
187
|
+
const callbackArg = node.arguments[1];
|
|
188
|
+
if (callbackArg && callbackArg.type === "ArrowFunctionExpression") {
|
|
189
|
+
|
|
190
|
+
let serviceCall = callbackArg.body;
|
|
191
|
+
|
|
192
|
+
// Handle async functions with BlockStatement body
|
|
193
|
+
if (serviceCall.type === "BlockStatement" && serviceCall.body.length > 0) {
|
|
194
|
+
const returnStmt = serviceCall.body.find(stmt => stmt.type === "ReturnStatement");
|
|
195
|
+
if (returnStmt && returnStmt.argument) {
|
|
196
|
+
serviceCall = returnStmt.argument;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Check if it's a direct CallExpression
|
|
201
|
+
if (serviceCall && serviceCall.type === "CallExpression") {
|
|
202
|
+
if (serviceCall.callee.type === "MemberExpression") {
|
|
203
|
+
let methodName = "";
|
|
204
|
+
|
|
205
|
+
// Handle this.serviceProperty.methodName() pattern
|
|
206
|
+
if (serviceCall.callee.object.type === "MemberExpression" &&
|
|
207
|
+
serviceCall.callee.object.object.type === "ThisExpression" &&
|
|
208
|
+
serviceCall.callee.property.name) {
|
|
209
|
+
methodName = serviceCall.callee.property.name;
|
|
210
|
+
}
|
|
211
|
+
// Handle this.methodName() pattern
|
|
212
|
+
else if (serviceCall.callee.object.type === "ThisExpression" &&
|
|
213
|
+
serviceCall.callee.property.name) {
|
|
214
|
+
methodName = serviceCall.callee.property.name;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
if (methodName) {
|
|
218
|
+
// Prüfe nur ob die Service-Klasse auf Entity endet
|
|
219
|
+
const isEntitySuspicious = serviceCall.callee.object.type === "MemberExpression" &&
|
|
220
|
+
serviceCall.callee.object.object.type === "ThisExpression" &&
|
|
221
|
+
serviceCall.callee.object.property.name.endsWith("Entity");
|
|
222
|
+
|
|
223
|
+
if (isEntitySuspicious) {
|
|
224
|
+
context.report({
|
|
225
|
+
node: serviceCall,
|
|
226
|
+
messageId: "serviceReturnsEntity",
|
|
227
|
+
data: {
|
|
228
|
+
methodName: methodName,
|
|
229
|
+
},
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
},
|
|
238
|
+
};
|
|
239
|
+
},
|
|
240
|
+
};
|
|
241
|
+
|
|
242
|
+
/** @type {import('eslint').Rule.RuleModule} */
|
|
243
|
+
const requireProperResponseHandlingRule = {
|
|
244
|
+
meta: {
|
|
245
|
+
type: "problem",
|
|
246
|
+
docs: {
|
|
247
|
+
description: "Controller müssen sendSuccess/sendError für Responses nutzen",
|
|
248
|
+
category: "Architecture",
|
|
249
|
+
recommended: true,
|
|
250
|
+
},
|
|
251
|
+
schema: [],
|
|
252
|
+
messages: {
|
|
253
|
+
useBaseControllerMethods: "Verwende this.sendSuccess() oder this.sendError() anstatt direkter Response-Methoden: '{{method}}'",
|
|
254
|
+
noDirectReturn: "Controller-Methoden dürfen nicht direkt Werte zurückgeben. Verwende this.sendSuccess(): '{{returnType}}'",
|
|
255
|
+
},
|
|
256
|
+
},
|
|
257
|
+
create(context) {
|
|
258
|
+
const controllerFiles = /Controller\.ts$/;
|
|
259
|
+
const filename = context.getFilename();
|
|
260
|
+
const isControllerFile = controllerFiles.test(filename) &&
|
|
261
|
+
!/BaseController\.ts$/.test(filename);
|
|
262
|
+
|
|
263
|
+
if (!isControllerFile) return {};
|
|
264
|
+
|
|
265
|
+
return {
|
|
266
|
+
// Prüfe auf direkte res.json/res.status Aufrufe
|
|
267
|
+
CallExpression(node) {
|
|
268
|
+
// res.json() direkt
|
|
269
|
+
if (
|
|
270
|
+
node.callee.type === "MemberExpression" &&
|
|
271
|
+
node.callee.object.type === "Identifier" &&
|
|
272
|
+
node.callee.object.name === "res" &&
|
|
273
|
+
node.callee.property.type === "Identifier" &&
|
|
274
|
+
(node.callee.property.name === "json" ||
|
|
275
|
+
node.callee.property.name === "status" ||
|
|
276
|
+
node.callee.property.name === "send")
|
|
277
|
+
) {
|
|
278
|
+
context.report({
|
|
279
|
+
node,
|
|
280
|
+
messageId: "useBaseControllerMethods",
|
|
281
|
+
data: {
|
|
282
|
+
method: `res.${node.callee.property.name}()`,
|
|
283
|
+
},
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
},
|
|
287
|
+
|
|
288
|
+
// Prüfe Return-Statements in Controller-Methoden
|
|
289
|
+
MethodDefinition(node) {
|
|
290
|
+
// Nur öffentliche Methoden prüfen (nicht private oder protected)
|
|
291
|
+
if (node.accessibility === "private" || node.accessibility === "protected") {
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
if (node.value.type === "FunctionExpression" ||
|
|
296
|
+
node.value.type === "ArrowFunctionExpression") {
|
|
297
|
+
|
|
298
|
+
// Durchsuche den Function Body nach Return-Statements
|
|
299
|
+
const checkReturnStatements = (blockStatement) => {
|
|
300
|
+
if (!blockStatement || !blockStatement.body) return;
|
|
301
|
+
|
|
302
|
+
for (const stmt of blockStatement.body) {
|
|
303
|
+
if (stmt.type === "ReturnStatement" &&
|
|
304
|
+
stmt.argument &&
|
|
305
|
+
stmt.argument.type !== "CallExpression") {
|
|
306
|
+
|
|
307
|
+
// Ignoriere "return;" ohne Wert
|
|
308
|
+
if (stmt.argument.type === "Identifier" &&
|
|
309
|
+
stmt.argument.name === "undefined") continue;
|
|
310
|
+
|
|
311
|
+
context.report({
|
|
312
|
+
node: stmt,
|
|
313
|
+
messageId: "noDirectReturn",
|
|
314
|
+
data: {
|
|
315
|
+
returnType: "value",
|
|
316
|
+
},
|
|
317
|
+
});
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
};
|
|
321
|
+
|
|
322
|
+
if (node.value.body.type === "BlockStatement") {
|
|
323
|
+
checkReturnStatements(node.value.body);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
},
|
|
327
|
+
};
|
|
328
|
+
},
|
|
329
|
+
};
|
|
330
|
+
|
|
331
|
+
// Export-Objekt mit allen Regeln
|
|
332
|
+
const controllerArchitectureRules = {
|
|
333
|
+
rules: {
|
|
334
|
+
"controller-architecture": noDirectRepositoryUseRule,
|
|
335
|
+
"require-dto-response": requireDtoResponseRule,
|
|
336
|
+
"require-proper-response-handling": requireProperResponseHandlingRule,
|
|
337
|
+
},
|
|
338
|
+
};
|
|
339
|
+
|
|
340
|
+
export default controllerArchitectureRules;
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Enforce consistent naming conventions for controller methods
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
"use strict";
|
|
6
|
+
|
|
7
|
+
export default {
|
|
8
|
+
meta: {
|
|
9
|
+
type: "problem",
|
|
10
|
+
docs: {
|
|
11
|
+
description: "enforce consistent naming conventions for controller HTTP methods",
|
|
12
|
+
category: "Best Practices",
|
|
13
|
+
recommended: true,
|
|
14
|
+
},
|
|
15
|
+
fixable: null,
|
|
16
|
+
schema: [],
|
|
17
|
+
messages: {
|
|
18
|
+
invalidMethodName: "Controller method '{{methodName}}' does not follow naming conventions. Use: getById (GET with params), getAll (GET without params), create (POST), update (PUT/PATCH), delete (DELETE)",
|
|
19
|
+
entityInResponse: "Entity-Variable '{{variableName}}' darf nicht direkt in sendSuccess() verwendet werden. Controller müssen Entities zu DTOs konvertieren bevor sie als API-Response zurückgegeben werden.",
|
|
20
|
+
},
|
|
21
|
+
},
|
|
22
|
+
|
|
23
|
+
create(context) {
|
|
24
|
+
const filename = context.getFilename();
|
|
25
|
+
|
|
26
|
+
if (!filename.includes("Controller.ts") || filename.includes("test") || filename.includes("spec")) {
|
|
27
|
+
return {};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function isControllerClass(node) {
|
|
31
|
+
if (node.type !== "ClassDeclaration") return false;
|
|
32
|
+
|
|
33
|
+
return node.decorators && node.decorators.some(decorator => {
|
|
34
|
+
return decorator.expression &&
|
|
35
|
+
((decorator.expression.type === "Identifier" && decorator.expression.name === "Controller") ||
|
|
36
|
+
(decorator.expression.type === "CallExpression" &&
|
|
37
|
+
decorator.expression.callee.name === "Controller"));
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function getHttpMethodType(decorators) {
|
|
42
|
+
if (!decorators) return null;
|
|
43
|
+
|
|
44
|
+
const httpMethodMap = {
|
|
45
|
+
"Get": "get",
|
|
46
|
+
"Post": "post",
|
|
47
|
+
"Put": "put",
|
|
48
|
+
"Delete": "delete",
|
|
49
|
+
"Patch": "patch"
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
for (const decorator of decorators) {
|
|
53
|
+
const decoratorName = decorator.expression?.name || decorator.expression?.callee?.name;
|
|
54
|
+
if (httpMethodMap[decoratorName]) {
|
|
55
|
+
let route = "";
|
|
56
|
+
let hasParameter = false;
|
|
57
|
+
|
|
58
|
+
if (decorator.expression?.type === "CallExpression" &&
|
|
59
|
+
decorator.expression.arguments.length > 0 &&
|
|
60
|
+
decorator.expression.arguments[0].type === "Literal") {
|
|
61
|
+
route = decorator.expression.arguments[0].value;
|
|
62
|
+
hasParameter = route.includes(":");
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return {
|
|
66
|
+
type: httpMethodMap[decoratorName],
|
|
67
|
+
hasParameter,
|
|
68
|
+
route,
|
|
69
|
+
isSubResource: route.includes("/") && route.includes(":")
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function validateMethodName(methodName, httpMethod) {
|
|
78
|
+
const conventions = {
|
|
79
|
+
get: {
|
|
80
|
+
withParam: "getById",
|
|
81
|
+
withoutParam: "getAll"
|
|
82
|
+
},
|
|
83
|
+
post: "create",
|
|
84
|
+
put: "update",
|
|
85
|
+
delete: "delete",
|
|
86
|
+
patch: "update"
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
// Allow flexible naming for sub-resource routes (e.g., ":id/effects", ":id/assignments")
|
|
90
|
+
if (httpMethod.isSubResource) {
|
|
91
|
+
return true;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (httpMethod.type === "get") {
|
|
95
|
+
const expectedName = httpMethod.hasParameter ?
|
|
96
|
+
conventions.get.withParam :
|
|
97
|
+
conventions.get.withoutParam;
|
|
98
|
+
return methodName === expectedName;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return methodName === conventions[httpMethod.type];
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function getExpectedName(httpMethod) {
|
|
105
|
+
if (httpMethod.type === "get") {
|
|
106
|
+
return httpMethod.hasParameter ? "getById" : "getAll";
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const expectedNames = {
|
|
110
|
+
post: "create",
|
|
111
|
+
put: "update",
|
|
112
|
+
delete: "delete",
|
|
113
|
+
patch: "update"
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
return expectedNames[httpMethod.type];
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Entity-Pattern erkennen
|
|
120
|
+
function isEntityPattern(variableName) {
|
|
121
|
+
return (
|
|
122
|
+
variableName.endsWith("Definition") ||
|
|
123
|
+
variableName.endsWith("Entity") ||
|
|
124
|
+
variableName.endsWith("Model") ||
|
|
125
|
+
variableName.endsWith("Record") ||
|
|
126
|
+
variableName.endsWith("Instance") ||
|
|
127
|
+
variableName.startsWith("new") ||
|
|
128
|
+
variableName.includes("Entity") ||
|
|
129
|
+
variableName.includes("Definition")
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return {
|
|
134
|
+
ClassDeclaration(node) {
|
|
135
|
+
if (!isControllerClass(node)) return;
|
|
136
|
+
|
|
137
|
+
node.body.body.forEach(member => {
|
|
138
|
+
if (member.type === "MethodDefinition" &&
|
|
139
|
+
member.kind === "method" &&
|
|
140
|
+
member.accessibility === "public") {
|
|
141
|
+
|
|
142
|
+
const httpMethod = getHttpMethodType(member.decorators);
|
|
143
|
+
if (!httpMethod) return;
|
|
144
|
+
|
|
145
|
+
const methodName = member.key.name;
|
|
146
|
+
|
|
147
|
+
if (!validateMethodName(methodName, httpMethod)) {
|
|
148
|
+
const expectedName = getExpectedName(httpMethod);
|
|
149
|
+
|
|
150
|
+
context.report({
|
|
151
|
+
node: member,
|
|
152
|
+
message: `Controller method '${methodName}' must be named '${expectedName}' for ${httpMethod.type.toUpperCase()} methods. Use consistent naming across all controllers.`,
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
});
|
|
157
|
+
},
|
|
158
|
+
|
|
159
|
+
// Prüfe Entity-Patterns in sendSuccess
|
|
160
|
+
CallExpression(node) {
|
|
161
|
+
if (node.callee?.type === "MemberExpression" &&
|
|
162
|
+
node.callee.object?.type === "ThisExpression" &&
|
|
163
|
+
node.callee.property?.name === "sendSuccess") {
|
|
164
|
+
|
|
165
|
+
if (node.arguments.length >= 2 &&
|
|
166
|
+
node.arguments[1]?.type === "ObjectExpression") {
|
|
167
|
+
|
|
168
|
+
const responseObject = node.arguments[1];
|
|
169
|
+
|
|
170
|
+
responseObject.properties.forEach(property => {
|
|
171
|
+
if (property.type === "Property" &&
|
|
172
|
+
property.value?.type === "Identifier") {
|
|
173
|
+
|
|
174
|
+
const variableName = property.value.name;
|
|
175
|
+
|
|
176
|
+
if (isEntityPattern(variableName)) {
|
|
177
|
+
context.report({
|
|
178
|
+
node: property.value,
|
|
179
|
+
messageId: "entityInResponse",
|
|
180
|
+
data: { variableName }
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
};
|
|
189
|
+
},
|
|
190
|
+
};
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ESLint-Regel: Controller Readonly Restriction
|
|
3
|
+
* Normale Controller (nicht Admin) dürfen nur GET-Methoden haben.
|
|
4
|
+
* Schreibende Zugriffe (POST, PUT, DELETE) erfolgen über Socket.io.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/** @type {import('eslint').Rule.RuleModule} */
|
|
8
|
+
const noWriteMethodsInPublicControllersRule = {
|
|
9
|
+
meta: {
|
|
10
|
+
type: "problem",
|
|
11
|
+
docs: {
|
|
12
|
+
description: "Normale Controller dürfen nur GET-Methoden haben. Schreibende Zugriffe erfolgen über Socket.io.",
|
|
13
|
+
category: "Architecture",
|
|
14
|
+
recommended: true,
|
|
15
|
+
},
|
|
16
|
+
schema: [
|
|
17
|
+
{
|
|
18
|
+
type: "object",
|
|
19
|
+
properties: {
|
|
20
|
+
exceptions: {
|
|
21
|
+
type: "array",
|
|
22
|
+
items: {
|
|
23
|
+
type: "string"
|
|
24
|
+
},
|
|
25
|
+
description: "Array von Datei-Pfaden oder Mustern, die von dieser Regel ausgenommen werden sollen"
|
|
26
|
+
}
|
|
27
|
+
},
|
|
28
|
+
additionalProperties: false
|
|
29
|
+
}
|
|
30
|
+
],
|
|
31
|
+
messages: {
|
|
32
|
+
noWriteMethods: "Normale Controller dürfen nur GET-Methoden haben. {{method}}-Methoden sind nicht erlaubt. Verwende Socket.io für schreibende Zugriffe.",
|
|
33
|
+
noWriteDecorators: "Normale Controller dürfen keine {{decorator}}-Decorators haben. Verwende Socket.io für schreibende Zugriffe.",
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
create(context) {
|
|
37
|
+
const controllerFiles = /Controller\.ts$/;
|
|
38
|
+
const filename = context.getFilename();
|
|
39
|
+
const isControllerFile = controllerFiles.test(filename) &&
|
|
40
|
+
!/BaseController\.ts$/.test(filename);
|
|
41
|
+
|
|
42
|
+
if (!isControllerFile) return {};
|
|
43
|
+
|
|
44
|
+
// Hole die Konfiguration für Ausnahmen
|
|
45
|
+
const options = context.options[0] || {};
|
|
46
|
+
const exceptions = options.exceptions || [];
|
|
47
|
+
|
|
48
|
+
// Prüfe ob die aktuelle Datei in den Ausnahmen ist
|
|
49
|
+
const isException = exceptions.some(exception => {
|
|
50
|
+
// Unterstütze sowohl exakte Pfade als auch Muster
|
|
51
|
+
if (exception.includes('*')) {
|
|
52
|
+
// Einfache Glob-Pattern-Unterstützung
|
|
53
|
+
const pattern = new RegExp(exception.replace(/\*/g, '.*'));
|
|
54
|
+
return pattern.test(filename);
|
|
55
|
+
} else {
|
|
56
|
+
// Exakter Pfad-Vergleich (auch relative Pfade)
|
|
57
|
+
return filename.includes(exception) || filename.endsWith(exception);
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
// Wenn die Datei eine Ausnahme ist, überspringe die Prüfung
|
|
62
|
+
if (isException) return {};
|
|
63
|
+
|
|
64
|
+
// Prüfe ob es ein Admin-Controller ist
|
|
65
|
+
const isAdminController = filename.includes("/Admin/") ||
|
|
66
|
+
filename.includes("AdminController") ||
|
|
67
|
+
context.getSourceCode().getText().includes("@UseGuards(AuthGuard, AdminGuard)");
|
|
68
|
+
|
|
69
|
+
// Normale Controller (nicht Admin) werden geprüft
|
|
70
|
+
if (isAdminController) return {};
|
|
71
|
+
|
|
72
|
+
const writeMethods = ["POST", "PUT", "PATCH", "DELETE"];
|
|
73
|
+
const writeDecorators = ["@Post", "@Put", "@Patch", "@Delete"];
|
|
74
|
+
|
|
75
|
+
return {
|
|
76
|
+
// Prüfe auf HTTP-Method-Decorators
|
|
77
|
+
Decorator(node) {
|
|
78
|
+
if (node.expression.type === "CallExpression" &&
|
|
79
|
+
node.expression.callee.type === "Identifier") {
|
|
80
|
+
|
|
81
|
+
const decoratorName = node.expression.callee.name;
|
|
82
|
+
|
|
83
|
+
if (writeDecorators.includes(`@${decoratorName}`)) {
|
|
84
|
+
context.report({
|
|
85
|
+
node,
|
|
86
|
+
messageId: "noWriteDecorators",
|
|
87
|
+
data: {
|
|
88
|
+
decorator: `@${decoratorName}`,
|
|
89
|
+
},
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
},
|
|
94
|
+
|
|
95
|
+
// Prüfe auf Methodennamen die schreibende Operationen implizieren
|
|
96
|
+
MethodDefinition(node) {
|
|
97
|
+
if (node.key.type === "Identifier") {
|
|
98
|
+
const methodName = node.key.name;
|
|
99
|
+
|
|
100
|
+
// Prüfe auf typische schreibende Methodennamen
|
|
101
|
+
const writeMethodNames = [
|
|
102
|
+
"create", "post", "add", "insert",
|
|
103
|
+
"update", "put", "patch", "edit", "modify",
|
|
104
|
+
"delete", "remove", "destroy", "drop"
|
|
105
|
+
];
|
|
106
|
+
|
|
107
|
+
if (writeMethodNames.some(writeMethod =>
|
|
108
|
+
methodName.toLowerCase().includes(writeMethod))) {
|
|
109
|
+
|
|
110
|
+
context.report({
|
|
111
|
+
node,
|
|
112
|
+
messageId: "noWriteMethods",
|
|
113
|
+
data: {
|
|
114
|
+
method: methodName,
|
|
115
|
+
},
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
},
|
|
120
|
+
|
|
121
|
+
// Prüfe auf handleCreate, handleUpdate, handleDelete Aufrufe
|
|
122
|
+
CallExpression(node) {
|
|
123
|
+
if (node.callee.type === "MemberExpression" &&
|
|
124
|
+
node.callee.object.type === "ThisExpression" &&
|
|
125
|
+
node.callee.property.type === "Identifier") {
|
|
126
|
+
|
|
127
|
+
const methodName = node.callee.property.name;
|
|
128
|
+
|
|
129
|
+
if (["handleCreate", "handleUpdate", "handleDelete"].includes(methodName)) {
|
|
130
|
+
context.report({
|
|
131
|
+
node,
|
|
132
|
+
messageId: "noWriteMethods",
|
|
133
|
+
data: {
|
|
134
|
+
method: `this.${methodName}()`,
|
|
135
|
+
},
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
},
|
|
140
|
+
};
|
|
141
|
+
},
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
export default {
|
|
145
|
+
rules: {
|
|
146
|
+
"no-write-methods-in-public-controllers": noWriteMethodsInPublicControllersRule,
|
|
147
|
+
},
|
|
148
|
+
};
|