@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,240 @@
|
|
|
1
|
+
const enforceViteHealthMetricsRule = {
|
|
2
|
+
meta: {
|
|
3
|
+
type: "problem",
|
|
4
|
+
docs: {
|
|
5
|
+
description: "Enforce Vite configuration to include required health and metrics endpoints",
|
|
6
|
+
category: "Best Practices",
|
|
7
|
+
recommended: true,
|
|
8
|
+
},
|
|
9
|
+
fixable: null,
|
|
10
|
+
schema: [{
|
|
11
|
+
type: "object",
|
|
12
|
+
properties: {
|
|
13
|
+
requiredEndpoints: {
|
|
14
|
+
type: "array",
|
|
15
|
+
items: { type: "string" },
|
|
16
|
+
default: ["/health", "/health/live", "/health/ready", "/metrics"],
|
|
17
|
+
},
|
|
18
|
+
requiredPackages: {
|
|
19
|
+
type: "array",
|
|
20
|
+
items: { type: "string" },
|
|
21
|
+
default: ["@godaddy/terminus", "prom-client"],
|
|
22
|
+
},
|
|
23
|
+
requiredServices: {
|
|
24
|
+
type: "array",
|
|
25
|
+
items: { type: "string" },
|
|
26
|
+
default: ["HealthMetricsService", "MetricsService"],
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
additionalProperties: false,
|
|
30
|
+
}],
|
|
31
|
+
messages: {
|
|
32
|
+
missingHealthEndpoint: "Missing required health endpoint: {{endpoint}}. Add middleware for {{endpoint}} in Vite config.",
|
|
33
|
+
missingMetricsEndpoint: "Missing required metrics endpoint: {{endpoint}}. Add middleware for {{endpoint}} in Vite config.",
|
|
34
|
+
missingPackage: "Missing required package: {{package}}. Install {{package}} for health/metrics functionality.",
|
|
35
|
+
missingService: "Missing required service: {{service}}. Import and use {{service}} in Vite config.",
|
|
36
|
+
incorrectContentType: "Health endpoint {{endpoint}} should return 'text/plain' content type for Prometheus metrics.",
|
|
37
|
+
missingConfigureServer: "Vite config must include configureServer function for health/metrics endpoints.",
|
|
38
|
+
missingMiddlewares: "Vite config must include server.middlewares.use for health/metrics endpoints.",
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
|
|
42
|
+
create(context) {
|
|
43
|
+
const options = context.options[0] || {};
|
|
44
|
+
const requiredEndpoints = options.requiredEndpoints || ["/health", "/health/live", "/health/ready", "/metrics"];
|
|
45
|
+
const requiredPackages = options.requiredPackages || ["@godaddy/terminus", "prom-client"];
|
|
46
|
+
const requiredServices = options.requiredServices || ["HealthMetricsService", "MetricsService"];
|
|
47
|
+
|
|
48
|
+
let hasViteConfig = false;
|
|
49
|
+
let hasConfigureServer = false;
|
|
50
|
+
let hasMiddlewares = false;
|
|
51
|
+
let foundEndpoints = new Set();
|
|
52
|
+
let foundServices = new Set();
|
|
53
|
+
let foundPackages = new Set();
|
|
54
|
+
|
|
55
|
+
return {
|
|
56
|
+
// Check for Vite config file
|
|
57
|
+
Program() {
|
|
58
|
+
const filename = context.getFilename();
|
|
59
|
+
if (filename.includes("vite.config") || filename.includes("vite.config.ts") || filename.includes("vite.config.js")) {
|
|
60
|
+
hasViteConfig = true;
|
|
61
|
+
}
|
|
62
|
+
},
|
|
63
|
+
|
|
64
|
+
// Check for import statements
|
|
65
|
+
ImportDeclaration(node) {
|
|
66
|
+
const source = node.source.value;
|
|
67
|
+
if (requiredPackages.includes(source)) {
|
|
68
|
+
foundPackages.add(source);
|
|
69
|
+
}
|
|
70
|
+
},
|
|
71
|
+
|
|
72
|
+
// Check for configureServer function
|
|
73
|
+
FunctionDeclaration(node) {
|
|
74
|
+
if (node.id && node.id.name === "configureServer") {
|
|
75
|
+
hasConfigureServer = true;
|
|
76
|
+
}
|
|
77
|
+
},
|
|
78
|
+
|
|
79
|
+
// Check for ArrowFunctionExpression with configureServer
|
|
80
|
+
ArrowFunctionExpression(node) {
|
|
81
|
+
if (node.parent && node.parent.type === "Property" && node.parent.key && node.parent.key.name === "configureServer") {
|
|
82
|
+
hasConfigureServer = true;
|
|
83
|
+
}
|
|
84
|
+
},
|
|
85
|
+
|
|
86
|
+
// Check for server.middlewares.use calls and content type headers
|
|
87
|
+
CallExpression(node) {
|
|
88
|
+
// Check for server.middlewares.use calls
|
|
89
|
+
if (node.callee.type === "MemberExpression" &&
|
|
90
|
+
node.callee.object.type === "MemberExpression" &&
|
|
91
|
+
node.callee.object.object.name === "server" &&
|
|
92
|
+
node.callee.object.property.name === "middlewares" &&
|
|
93
|
+
node.callee.property.name === "use") {
|
|
94
|
+
|
|
95
|
+
hasMiddlewares = true;
|
|
96
|
+
|
|
97
|
+
// Check for endpoint path
|
|
98
|
+
const endpointArg = node.arguments[0];
|
|
99
|
+
if (endpointArg && endpointArg.type === "Literal" && typeof endpointArg.value === "string") {
|
|
100
|
+
const endpoint = endpointArg.value;
|
|
101
|
+
if (requiredEndpoints.includes(endpoint)) {
|
|
102
|
+
foundEndpoints.add(endpoint);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Check for content type headers
|
|
108
|
+
if (node.callee.type === "MemberExpression" &&
|
|
109
|
+
node.callee.property.name === "setHeader") {
|
|
110
|
+
|
|
111
|
+
const headerName = node.arguments[0];
|
|
112
|
+
const headerValue = node.arguments[1];
|
|
113
|
+
|
|
114
|
+
if (headerName && headerName.type === "Literal" && headerName.value === "Content-Type") {
|
|
115
|
+
if (headerValue && headerValue.type === "Literal" && headerValue.value === "application/json") {
|
|
116
|
+
// Find the endpoint this belongs to by traversing up
|
|
117
|
+
let current = node.parent;
|
|
118
|
+
while (current && current.type !== "CallExpression") {
|
|
119
|
+
current = current.parent;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (current && current.callee.type === "MemberExpression" &&
|
|
123
|
+
current.callee.property.name === "use") {
|
|
124
|
+
const endpointArg = current.arguments[0];
|
|
125
|
+
if (endpointArg && endpointArg.type === "Literal" &&
|
|
126
|
+
(endpointArg.value === "/health" || endpointArg.value === "/metrics")) {
|
|
127
|
+
context.report({
|
|
128
|
+
node: headerValue,
|
|
129
|
+
messageId: "incorrectContentType",
|
|
130
|
+
data: { endpoint: endpointArg.value }
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
},
|
|
138
|
+
|
|
139
|
+
// Check for service imports and usage
|
|
140
|
+
VariableDeclarator(node) {
|
|
141
|
+
if (node.init && node.init.type === "CallExpression" &&
|
|
142
|
+
node.init.callee.type === "Identifier" &&
|
|
143
|
+
node.init.callee.name === "import") {
|
|
144
|
+
|
|
145
|
+
const importArg = node.init.arguments[0];
|
|
146
|
+
if (importArg && importArg.type === "Literal" && typeof importArg.value === "string") {
|
|
147
|
+
const importPath = importArg.value;
|
|
148
|
+
if (importPath.includes("HealthMetricsService") || importPath.includes("MetricsService")) {
|
|
149
|
+
requiredServices.forEach(service => {
|
|
150
|
+
if (importPath.includes(service)) {
|
|
151
|
+
foundServices.add(service);
|
|
152
|
+
}
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
},
|
|
158
|
+
|
|
159
|
+
// Check for service instantiation
|
|
160
|
+
NewExpression(node) {
|
|
161
|
+
if (node.callee.type === "Identifier") {
|
|
162
|
+
const serviceName = node.callee.name;
|
|
163
|
+
if (requiredServices.includes(serviceName)) {
|
|
164
|
+
foundServices.add(serviceName);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
},
|
|
168
|
+
|
|
169
|
+
// Final validation on program end
|
|
170
|
+
"Program:exit"() {
|
|
171
|
+
if (!hasViteConfig) {
|
|
172
|
+
return; // Not a Vite config file
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Check for missing packages
|
|
176
|
+
requiredPackages.forEach(packageName => {
|
|
177
|
+
if (!foundPackages.has(packageName)) {
|
|
178
|
+
context.report({
|
|
179
|
+
node: context.getSourceCode().ast,
|
|
180
|
+
messageId: "missingPackage",
|
|
181
|
+
data: { package: packageName }
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
// Check for missing services
|
|
187
|
+
requiredServices.forEach(serviceName => {
|
|
188
|
+
if (!foundServices.has(serviceName)) {
|
|
189
|
+
context.report({
|
|
190
|
+
node: context.getSourceCode().ast,
|
|
191
|
+
messageId: "missingService",
|
|
192
|
+
data: { service: serviceName }
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
// Check for missing endpoints
|
|
198
|
+
requiredEndpoints.forEach(endpoint => {
|
|
199
|
+
if (!foundEndpoints.has(endpoint)) {
|
|
200
|
+
if (endpoint === "/health" || endpoint === "/metrics") {
|
|
201
|
+
context.report({
|
|
202
|
+
node: context.getSourceCode().ast,
|
|
203
|
+
messageId: "missingMetricsEndpoint",
|
|
204
|
+
data: { endpoint }
|
|
205
|
+
});
|
|
206
|
+
} else {
|
|
207
|
+
context.report({
|
|
208
|
+
node: context.getSourceCode().ast,
|
|
209
|
+
messageId: "missingHealthEndpoint",
|
|
210
|
+
data: { endpoint }
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
// Check for configureServer function
|
|
217
|
+
if (!hasConfigureServer) {
|
|
218
|
+
context.report({
|
|
219
|
+
node: context.getSourceCode().ast,
|
|
220
|
+
messageId: "missingConfigureServer"
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Check for middlewares
|
|
225
|
+
if (!hasMiddlewares) {
|
|
226
|
+
context.report({
|
|
227
|
+
node: context.getSourceCode().ast,
|
|
228
|
+
messageId: "missingMiddlewares"
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
export default {
|
|
237
|
+
rules: {
|
|
238
|
+
"enforce-vite-health-metrics": enforceViteHealthMetricsRule,
|
|
239
|
+
},
|
|
240
|
+
};
|
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Stellt sicher, dass alle required Properties einer Entity beim Erstellen einer Instanz gesetzt werden
|
|
3
|
+
* Diese Regel analysiert Entity-Klassen und prüft, ob bei `new EntityName()` alle nicht-optionalen Properties zugewiesen werden
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import fs from "fs";
|
|
7
|
+
import path from "path";
|
|
8
|
+
|
|
9
|
+
/** @type {import('eslint').Rule.RuleModule} */
|
|
10
|
+
const entityRequiredPropertiesRule = {
|
|
11
|
+
meta: {
|
|
12
|
+
type: "problem",
|
|
13
|
+
docs: {
|
|
14
|
+
description: "Stellt sicher, dass alle required Properties einer Entity beim Erstellen einer Instanz gesetzt werden",
|
|
15
|
+
category: "TypeORM",
|
|
16
|
+
recommended: true,
|
|
17
|
+
},
|
|
18
|
+
hasSuggestions: true,
|
|
19
|
+
schema: [],
|
|
20
|
+
messages: {
|
|
21
|
+
missingRequiredProperties: "Entity '{{entityName}}' Instanz fehlen required Properties: {{missingProps}}. Diese müssen nach 'new {{entityName}}()' gesetzt werden.",
|
|
22
|
+
optionalPropertyInfo: "Optional properties für '{{entityName}}': {{optionalProps}}",
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
|
|
26
|
+
create(context) {
|
|
27
|
+
const sourceCode = context.getSourceCode();
|
|
28
|
+
const filename = context.getFilename();
|
|
29
|
+
|
|
30
|
+
// Nur Service-Dateien und Controller prüfen (dort werden meist Entities erstellt)
|
|
31
|
+
if (!filename.includes("/service/") && !filename.includes("/controller/")) {
|
|
32
|
+
return {};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Analysiert Entity-Klasse und extrahiert required/optional Properties (String-basiert)
|
|
37
|
+
*/
|
|
38
|
+
function analyzeEntityClass(entityFilePath) {
|
|
39
|
+
const required = [];
|
|
40
|
+
const optional = [];
|
|
41
|
+
|
|
42
|
+
try {
|
|
43
|
+
const content = fs.readFileSync(entityFilePath, "utf8");
|
|
44
|
+
|
|
45
|
+
// Finde Klassen-Definition
|
|
46
|
+
const classMatch = content.match(/export\s+class\s+(\w+Entity)/);
|
|
47
|
+
if (!classMatch) {
|
|
48
|
+
return { required, optional };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const className = classMatch[1];
|
|
52
|
+
|
|
53
|
+
// Teile Content in Zeilen und analysiere Properties
|
|
54
|
+
const lines = content.split('\n');
|
|
55
|
+
let insideClass = false;
|
|
56
|
+
let currentDecorators = [];
|
|
57
|
+
|
|
58
|
+
for (let i = 0; i < lines.length; i++) {
|
|
59
|
+
const line = lines[i].trim();
|
|
60
|
+
|
|
61
|
+
// Starte bei Klassendefinition
|
|
62
|
+
if (line.includes(`class ${className}`)) {
|
|
63
|
+
insideClass = true;
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (!insideClass) continue;
|
|
68
|
+
|
|
69
|
+
// Ende der Klasse
|
|
70
|
+
if (line === '}') {
|
|
71
|
+
break;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Decorator erkennen (kann mehrzeilig sein)
|
|
75
|
+
if (line.startsWith('@')) {
|
|
76
|
+
const decoratorMatch = line.match(/@(\w+)/);
|
|
77
|
+
if (decoratorMatch) {
|
|
78
|
+
let decoratorContent = line;
|
|
79
|
+
|
|
80
|
+
// Wenn Decorator mehrzeilig ist, sammle alle Zeilen bis zur schließenden Klammer
|
|
81
|
+
let j = i + 1;
|
|
82
|
+
let openBraces = (line.match(/\{/g) || []).length - (line.match(/\}/g) || []).length;
|
|
83
|
+
let openParens = (line.match(/\(/g) || []).length - (line.match(/\)/g) || []).length;
|
|
84
|
+
|
|
85
|
+
while (j < lines.length && (openBraces > 0 || openParens > 0)) {
|
|
86
|
+
const nextLine = lines[j].trim();
|
|
87
|
+
decoratorContent += " " + nextLine;
|
|
88
|
+
|
|
89
|
+
openBraces += (nextLine.match(/\{/g) || []).length - (nextLine.match(/\}/g) || []).length;
|
|
90
|
+
openParens += (nextLine.match(/\(/g) || []).length - (nextLine.match(/\)/g) || []).length;
|
|
91
|
+
|
|
92
|
+
j++;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
currentDecorators.push({
|
|
96
|
+
name: decoratorMatch[1],
|
|
97
|
+
full: decoratorContent
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
// Springe über verarbeitete Zeilen
|
|
101
|
+
i = j - 1;
|
|
102
|
+
}
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Property erkennen - nur echte Klassenmember
|
|
107
|
+
const propertyMatch = line.match(/^(\w+)(\?)?\s*:\s*([^;{]+);?\s*$/);
|
|
108
|
+
if (propertyMatch && !line.includes('type:') && !line.includes('length:')) {
|
|
109
|
+
const propertyName = propertyMatch[1];
|
|
110
|
+
const isOptional = !!propertyMatch[2]; // hat ?
|
|
111
|
+
const propertyType = propertyMatch[3];
|
|
112
|
+
|
|
113
|
+
const isRequired = analyzePropertyRequirement(propertyName, isOptional, propertyType, currentDecorators);
|
|
114
|
+
|
|
115
|
+
if (isRequired) {
|
|
116
|
+
required.push(propertyName);
|
|
117
|
+
} else {
|
|
118
|
+
optional.push(propertyName);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Reset für nächstes Property
|
|
122
|
+
currentDecorators = [];
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
} catch (error) {
|
|
127
|
+
// Bei Fehlern keine Warnungen
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return { required, optional };
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Analysiert ob ein Property required ist (String-basiert)
|
|
135
|
+
*/
|
|
136
|
+
function analyzePropertyRequirement(propertyName, isOptional, propertyType, decorators) {
|
|
137
|
+
// TypeScript optional marker
|
|
138
|
+
if (isOptional) {
|
|
139
|
+
return false;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Nur Properties mit Decorators überwachen
|
|
143
|
+
if (decorators.length === 0) {
|
|
144
|
+
return false;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Automatische Properties
|
|
148
|
+
const autoGeneratedDecorators = ["PrimaryGeneratedColumn", "CreateDateColumn", "UpdateDateColumn"];
|
|
149
|
+
if (decorators.some(d => autoGeneratedDecorators.includes(d.name))) {
|
|
150
|
+
return false;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// @Column mit nullable oder default
|
|
154
|
+
const columnDecorator = decorators.find(d => d.name === "Column");
|
|
155
|
+
if (columnDecorator) {
|
|
156
|
+
const hasNullable = columnDecorator.full.includes("nullable: true");
|
|
157
|
+
const hasDefault = columnDecorator.full.includes("default:") || columnDecorator.full.includes("default {");
|
|
158
|
+
|
|
159
|
+
if (hasNullable || hasDefault) {
|
|
160
|
+
return false;
|
|
161
|
+
}
|
|
162
|
+
return true; // Column ohne nullable/default ist required
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Relationen
|
|
166
|
+
const relationDecorators = ["ManyToOne", "OneToOne", "OneToMany", "ManyToMany"];
|
|
167
|
+
const relationDecorator = decorators.find(d => relationDecorators.includes(d.name));
|
|
168
|
+
if (relationDecorator) {
|
|
169
|
+
const hasNullable = relationDecorator.full.includes("nullable: true");
|
|
170
|
+
const hasNullType = propertyType.includes("| null");
|
|
171
|
+
|
|
172
|
+
if (hasNullable || hasNullType) {
|
|
173
|
+
return false;
|
|
174
|
+
}
|
|
175
|
+
return true; // Relation ohne nullable ist required
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Andere Decorators (JoinColumn, etc.) sind optional
|
|
179
|
+
return false;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Findet Entity-Datei basierend auf Import
|
|
184
|
+
*/
|
|
185
|
+
function findEntityFile(entityName) {
|
|
186
|
+
const currentDir = path.dirname(filename);
|
|
187
|
+
const projectRoot = path.resolve(currentDir, "../../../..");
|
|
188
|
+
|
|
189
|
+
// Rekursive Dateisuche ohne externe Dependencies
|
|
190
|
+
function findFileRecursively(dir, fileName) {
|
|
191
|
+
try {
|
|
192
|
+
const files = fs.readdirSync(dir);
|
|
193
|
+
|
|
194
|
+
for (const file of files) {
|
|
195
|
+
const fullPath = path.join(dir, file);
|
|
196
|
+
const stat = fs.statSync(fullPath);
|
|
197
|
+
|
|
198
|
+
if (stat.isFile() && file === fileName) {
|
|
199
|
+
return fullPath;
|
|
200
|
+
} else if (stat.isDirectory()) {
|
|
201
|
+
const found = findFileRecursively(fullPath, fileName);
|
|
202
|
+
if (found) return found;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
} catch {
|
|
206
|
+
// Verzeichnis nicht zugänglich, weiter
|
|
207
|
+
}
|
|
208
|
+
return null;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Suche in entity-Verzeichnissen
|
|
212
|
+
const entitySearchDirs = [
|
|
213
|
+
path.resolve(projectRoot, "src", "entity"),
|
|
214
|
+
path.resolve(currentDir, "../../../entity"),
|
|
215
|
+
path.resolve(currentDir, "../../../../entity"),
|
|
216
|
+
];
|
|
217
|
+
|
|
218
|
+
for (const searchDir of entitySearchDirs) {
|
|
219
|
+
const found = findFileRecursively(searchDir, `${entityName}.ts`);
|
|
220
|
+
if (found) {
|
|
221
|
+
return found;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
return null;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Sammelt alle Property-Zuweisungen nach einer Entity-Instanziierung
|
|
230
|
+
*/
|
|
231
|
+
function collectPropertyAssignments(entityVariableName, startNode) {
|
|
232
|
+
const assignments = new Set();
|
|
233
|
+
const scope = sourceCode.getScope(startNode);
|
|
234
|
+
|
|
235
|
+
// Durchsuche nachfolgende Statements für Property-Zuweisungen
|
|
236
|
+
let parent = startNode.parent;
|
|
237
|
+
while (parent && parent.type !== "BlockStatement") {
|
|
238
|
+
parent = parent.parent;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
if (parent && parent.body) {
|
|
242
|
+
const startIndex = parent.body.indexOf(startNode.parent);
|
|
243
|
+
const followingStatements = parent.body.slice(startIndex + 1);
|
|
244
|
+
|
|
245
|
+
followingStatements.forEach(statement => {
|
|
246
|
+
if (statement.type === "ExpressionStatement" &&
|
|
247
|
+
statement.expression.type === "AssignmentExpression") {
|
|
248
|
+
const left = statement.expression.left;
|
|
249
|
+
if (left.type === "MemberExpression" &&
|
|
250
|
+
left.object?.name === entityVariableName) {
|
|
251
|
+
assignments.add(left.property?.name);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
return assignments;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
return {
|
|
261
|
+
VariableDeclarator(node) {
|
|
262
|
+
// Prüfe auf `new EntityName()` Pattern
|
|
263
|
+
if (node.init?.type === "NewExpression") {
|
|
264
|
+
const entityName = node.init.callee?.name;
|
|
265
|
+
|
|
266
|
+
// Prüfe ob es eine Entity ist (Name endet mit "Entity")
|
|
267
|
+
if (entityName && entityName.endsWith("Entity")) {
|
|
268
|
+
const variableName = node.id?.name;
|
|
269
|
+
|
|
270
|
+
if (variableName) {
|
|
271
|
+
// Finde Entity-Datei und analysiere required Properties
|
|
272
|
+
const entityFile = findEntityFile(entityName);
|
|
273
|
+
|
|
274
|
+
if (entityFile) {
|
|
275
|
+
const { required, optional } = analyzeEntityClass(entityFile);
|
|
276
|
+
|
|
277
|
+
if (required.length > 0) {
|
|
278
|
+
// Sammle Property-Zuweisungen nach der Instanziierung
|
|
279
|
+
const assignments = collectPropertyAssignments(variableName, node);
|
|
280
|
+
|
|
281
|
+
// Finde fehlende required Properties
|
|
282
|
+
const missingProperties = required.filter(prop => !assignments.has(prop));
|
|
283
|
+
|
|
284
|
+
if (missingProperties.length > 0) {
|
|
285
|
+
context.report({
|
|
286
|
+
node: node.init,
|
|
287
|
+
messageId: "missingRequiredProperties",
|
|
288
|
+
data: {
|
|
289
|
+
entityName,
|
|
290
|
+
missingProps: missingProperties.join(", ")
|
|
291
|
+
},
|
|
292
|
+
suggest: [
|
|
293
|
+
{
|
|
294
|
+
desc: `Add missing required properties for ${entityName}`,
|
|
295
|
+
fix(fixer) {
|
|
296
|
+
// Erstelle Vorschlag für fehlende Properties
|
|
297
|
+
const suggestions = missingProperties.map(prop =>
|
|
298
|
+
`${variableName}.${prop} = /* TODO: set value */;`
|
|
299
|
+
).join("\n");
|
|
300
|
+
|
|
301
|
+
return fixer.insertTextAfter(node.parent, `\n${suggestions}`);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
]
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
};
|
|
314
|
+
},
|
|
315
|
+
};
|
|
316
|
+
|
|
317
|
+
export default {
|
|
318
|
+
rules: {
|
|
319
|
+
"entity-required-properties": entityRequiredPropertiesRule,
|
|
320
|
+
},
|
|
321
|
+
};
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
export default {
|
|
2
|
+
meta: {
|
|
3
|
+
type: "problem",
|
|
4
|
+
docs: {
|
|
5
|
+
description: "Verhindert die direkte Verwendung von Entities in Controller-Responses und erzwingt DTOs",
|
|
6
|
+
category: "Security",
|
|
7
|
+
recommended: true,
|
|
8
|
+
},
|
|
9
|
+
fixable: null,
|
|
10
|
+
schema: [],
|
|
11
|
+
messages: {
|
|
12
|
+
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.",
|
|
13
|
+
},
|
|
14
|
+
},
|
|
15
|
+
|
|
16
|
+
create(context) {
|
|
17
|
+
const filename = context.getFilename();
|
|
18
|
+
|
|
19
|
+
// Nur Controller-Dateien prüfen
|
|
20
|
+
if (!filename.includes("Controller.ts") || filename.includes("test")) {
|
|
21
|
+
return {};
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Entity-Pattern erkennen
|
|
25
|
+
function isEntityPattern(variableName) {
|
|
26
|
+
return (
|
|
27
|
+
variableName.endsWith("Definition") ||
|
|
28
|
+
variableName.endsWith("Entity") ||
|
|
29
|
+
variableName.endsWith("Model") ||
|
|
30
|
+
variableName.endsWith("Record") ||
|
|
31
|
+
variableName.endsWith("Instance") ||
|
|
32
|
+
variableName.startsWith("new") ||
|
|
33
|
+
variableName.includes("Entity") ||
|
|
34
|
+
variableName.includes("Definition")
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return {
|
|
39
|
+
CallExpression(node) {
|
|
40
|
+
// Prüfe auf this.sendSuccess() Aufrufe
|
|
41
|
+
if (node.callee?.type === "MemberExpression" &&
|
|
42
|
+
node.callee.object?.type === "ThisExpression" &&
|
|
43
|
+
node.callee.property?.name === "sendSuccess") {
|
|
44
|
+
|
|
45
|
+
// Prüfe das zweite Argument (Response-Objekt)
|
|
46
|
+
if (node.arguments.length >= 2 &&
|
|
47
|
+
node.arguments[1]?.type === "ObjectExpression") {
|
|
48
|
+
|
|
49
|
+
const responseObject = node.arguments[1];
|
|
50
|
+
|
|
51
|
+
// Prüfe alle Properties im Response-Objekt
|
|
52
|
+
responseObject.properties.forEach(property => {
|
|
53
|
+
if (property.type === "Property" &&
|
|
54
|
+
property.value?.type === "Identifier") {
|
|
55
|
+
|
|
56
|
+
const variableName = property.value.name;
|
|
57
|
+
|
|
58
|
+
// Melde Entity-Patterns
|
|
59
|
+
if (isEntityPattern(variableName)) {
|
|
60
|
+
context.report({
|
|
61
|
+
node: property.value,
|
|
62
|
+
messageId: "entityInResponse",
|
|
63
|
+
data: { variableName }
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
},
|
|
73
|
+
};
|