@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,265 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Enforce Swagger attributes in DTOs and prohibit them in Entities based on folder structure
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
"use strict";
|
|
6
|
+
|
|
7
|
+
export default {
|
|
8
|
+
meta: {
|
|
9
|
+
type: "problem",
|
|
10
|
+
docs: {
|
|
11
|
+
description: "enforce @ApiProperty in DTOs and prohibit Swagger attributes in Entities",
|
|
12
|
+
category: "Architecture",
|
|
13
|
+
recommended: true,
|
|
14
|
+
},
|
|
15
|
+
fixable: "code",
|
|
16
|
+
schema: [],
|
|
17
|
+
messages: {
|
|
18
|
+
missingApiPropertyInDto: "DTO property '{{propertyName}}' is missing @ApiProperty decorator. All DTO properties must have Swagger documentation.",
|
|
19
|
+
forbiddenSwaggerInEntity: "Entity property '{{propertyName}}' has Swagger decorator '{{decoratorName}}'. Entities should not contain API documentation - use DTOs instead.",
|
|
20
|
+
missingApiPropertyImportInDto: "DTO file is missing 'ApiProperty' import from '@nestjs/swagger'. All DTO properties must be documented.",
|
|
21
|
+
unnecessarySwaggerImportInEntity: "Entity file imports Swagger decorators ('{{importName}}'). Entities should not contain API documentation - remove Swagger imports.",
|
|
22
|
+
germanDescriptionInApiProperty: "DTO property '{{propertyName}}' has German description in @ApiProperty. All API descriptions must be in English for international compatibility.",
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
|
|
26
|
+
create(context) {
|
|
27
|
+
const filename = context.getFilename();
|
|
28
|
+
const isDtoFile = filename.includes("/dto/") || filename.includes("\\dto\\");
|
|
29
|
+
const isEntityFile = filename.includes("/entity/") || filename.includes("\\entity\\");
|
|
30
|
+
|
|
31
|
+
if (!isDtoFile && !isEntityFile) {
|
|
32
|
+
return {};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
let hasApiPropertyImport = false;
|
|
36
|
+
let hasSwaggerImports = [];
|
|
37
|
+
|
|
38
|
+
function isSwaggerDecorator(decoratorName) {
|
|
39
|
+
const swaggerDecorators = [
|
|
40
|
+
"ApiProperty",
|
|
41
|
+
"ApiHideProperty",
|
|
42
|
+
"ApiPropertyOptional",
|
|
43
|
+
"ApiResponse",
|
|
44
|
+
"ApiOperation",
|
|
45
|
+
"ApiTags",
|
|
46
|
+
"ApiBearerAuth",
|
|
47
|
+
"ApiBody",
|
|
48
|
+
"ApiParam",
|
|
49
|
+
"ApiQuery"
|
|
50
|
+
];
|
|
51
|
+
return swaggerDecorators.includes(decoratorName);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function hasApiPropertyDecorator(decorators) {
|
|
55
|
+
if (!decorators) return false;
|
|
56
|
+
|
|
57
|
+
return decorators.some(decorator => {
|
|
58
|
+
const decoratorName = decorator.expression?.name || decorator.expression?.callee?.name;
|
|
59
|
+
return decoratorName === "ApiProperty" || decoratorName === "ApiPropertyOptional";
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function getSwaggerDecorators(decorators) {
|
|
64
|
+
if (!decorators) return [];
|
|
65
|
+
|
|
66
|
+
return decorators.filter(decorator => {
|
|
67
|
+
const decoratorName = decorator.expression?.name || decorator.expression?.callee?.name;
|
|
68
|
+
return isSwaggerDecorator(decoratorName);
|
|
69
|
+
}).map(decorator => decorator.expression?.name || decorator.expression?.callee?.name);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function isPublicProperty(member) {
|
|
73
|
+
// TypeScript class properties that are public (default) or explicitly public
|
|
74
|
+
return member.type === "PropertyDefinition" ||
|
|
75
|
+
(member.type === "TSParameterProperty" &&
|
|
76
|
+
(member.accessibility === "public" || !member.accessibility));
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function shouldCheckProperty(member) {
|
|
80
|
+
// Skip private/protected properties and constructor parameters
|
|
81
|
+
if (member.accessibility === "private" || member.accessibility === "protected") {
|
|
82
|
+
return false;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Skip static properties
|
|
86
|
+
if (member.static) {
|
|
87
|
+
return false;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Skip methods
|
|
91
|
+
if (member.type === "MethodDefinition") {
|
|
92
|
+
return false;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return isPublicProperty(member);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function containsGermanWords(text) {
|
|
99
|
+
if (!text || typeof text !== "string") return false;
|
|
100
|
+
|
|
101
|
+
// Verwende franc für automatische Spracherkennung
|
|
102
|
+
// franc erkennt über 400 Sprachen und ist sehr präzise
|
|
103
|
+
try {
|
|
104
|
+
// Importiere franc dynamisch für ESLint-Regel
|
|
105
|
+
const franc = require("franc");
|
|
106
|
+
|
|
107
|
+
// Mindestlänge für zuverlässige Erkennung
|
|
108
|
+
if (text.length < 10) {
|
|
109
|
+
// Für sehr kurze Texte: einfache Heuristik mit häufigen deutschen Wörtern
|
|
110
|
+
const shortGermanWords = ["der", "die", "das", "und", "für", "von", "mit", "ist", "sind", "hat", "haben", "wird", "werden"];
|
|
111
|
+
const lowercaseText = text.toLowerCase();
|
|
112
|
+
return shortGermanWords.some(word => new RegExp(`\\b${word}\\b`).test(lowercaseText));
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// franc gibt ISO 639-3 Codes zurück: 'deu' für Deutsch, 'eng' für Englisch
|
|
116
|
+
const detectedLanguage = franc(text);
|
|
117
|
+
|
|
118
|
+
// Prüfe auf deutsche Sprache (alle deutschen Varianten)
|
|
119
|
+
const germanLanguageCodes = ["deu", "gsw", "bar", "ksh", "pfl", "sli", "vmf", "wae"];
|
|
120
|
+
return germanLanguageCodes.includes(detectedLanguage);
|
|
121
|
+
|
|
122
|
+
} catch (error) {
|
|
123
|
+
// Fallback: Einfache Heuristik wenn franc nicht verfügbar ist
|
|
124
|
+
const commonGermanWords = ["der", "die", "das", "und", "oder", "für", "von", "zu", "mit", "bei", "ist", "sind", "wird", "werden", "benutzer", "anwender", "einstellung", "konfiguration", "beschreibung"];
|
|
125
|
+
const lowercaseText = text.toLowerCase();
|
|
126
|
+
return commonGermanWords.some(word => new RegExp(`\\b${word}\\b`).test(lowercaseText));
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function checkApiPropertyDescription(decorator, propertyName) {
|
|
131
|
+
if (!decorator.expression?.arguments?.[0]) return;
|
|
132
|
+
|
|
133
|
+
const optionsNode = decorator.expression.arguments[0];
|
|
134
|
+
if (optionsNode.type !== "ObjectExpression") return;
|
|
135
|
+
|
|
136
|
+
const descriptionProperty = optionsNode.properties.find(prop =>
|
|
137
|
+
prop.key?.name === "description" ||
|
|
138
|
+
(prop.key?.type === "Literal" && prop.key?.value === "description")
|
|
139
|
+
);
|
|
140
|
+
|
|
141
|
+
if (descriptionProperty && descriptionProperty.value?.type === "Literal") {
|
|
142
|
+
const description = descriptionProperty.value.value;
|
|
143
|
+
if (containsGermanWords(description)) {
|
|
144
|
+
context.report({
|
|
145
|
+
node: descriptionProperty,
|
|
146
|
+
messageId: "germanDescriptionInApiProperty",
|
|
147
|
+
data: {
|
|
148
|
+
propertyName: propertyName
|
|
149
|
+
}
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return {
|
|
156
|
+
ImportDeclaration(node) {
|
|
157
|
+
if (node.source.value === "@nestjs/swagger") {
|
|
158
|
+
node.specifiers.forEach(spec => {
|
|
159
|
+
const importName = spec.imported?.name || spec.local?.name;
|
|
160
|
+
|
|
161
|
+
if (importName === "ApiProperty" || importName === "ApiPropertyOptional") {
|
|
162
|
+
hasApiPropertyImport = true;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (isSwaggerDecorator(importName)) {
|
|
166
|
+
hasSwaggerImports.push(importName);
|
|
167
|
+
}
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
// Check for Entity files with Swagger imports
|
|
171
|
+
if (isEntityFile && hasSwaggerImports.length > 0) {
|
|
172
|
+
context.report({
|
|
173
|
+
node,
|
|
174
|
+
messageId: "unnecessarySwaggerImportInEntity",
|
|
175
|
+
data: {
|
|
176
|
+
importName: hasSwaggerImports.join(", ")
|
|
177
|
+
}
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
},
|
|
182
|
+
|
|
183
|
+
ClassDeclaration(node) {
|
|
184
|
+
if (!node.body || !node.body.body) return;
|
|
185
|
+
|
|
186
|
+
const classProperties = node.body.body.filter(shouldCheckProperty);
|
|
187
|
+
|
|
188
|
+
// For DTO files
|
|
189
|
+
if (isDtoFile) {
|
|
190
|
+
// Check if DTO has properties but no ApiProperty import
|
|
191
|
+
if (classProperties.length > 0 && !hasApiPropertyImport) {
|
|
192
|
+
context.report({
|
|
193
|
+
node,
|
|
194
|
+
messageId: "missingApiPropertyImportInDto",
|
|
195
|
+
fix (fixer) {
|
|
196
|
+
const sourceCode = context.getSourceCode();
|
|
197
|
+
const program = sourceCode.ast;
|
|
198
|
+
|
|
199
|
+
// Find the last import statement
|
|
200
|
+
const imports = program.body.filter(stmt => stmt.type === "ImportDeclaration");
|
|
201
|
+
|
|
202
|
+
if (imports.length > 0) {
|
|
203
|
+
const lastImport = imports[imports.length - 1];
|
|
204
|
+
return fixer.insertTextAfter(
|
|
205
|
+
lastImport,
|
|
206
|
+
'\nimport { ApiProperty } from "@nestjs/swagger";'
|
|
207
|
+
);
|
|
208
|
+
} else {
|
|
209
|
+
// Insert at the beginning of the file
|
|
210
|
+
return fixer.insertTextBefore(
|
|
211
|
+
program.body[0],
|
|
212
|
+
'import { ApiProperty } from "@nestjs/swagger";\n\n'
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
|
+
},
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// Check each property for @ApiProperty
|
|
220
|
+
classProperties.forEach(member => {
|
|
221
|
+
if (!hasApiPropertyDecorator(member.decorators)) {
|
|
222
|
+
const propertyName = member.key?.name || "unknown";
|
|
223
|
+
|
|
224
|
+
context.report({
|
|
225
|
+
node: member,
|
|
226
|
+
messageId: "missingApiPropertyInDto",
|
|
227
|
+
data: {
|
|
228
|
+
propertyName: propertyName
|
|
229
|
+
}
|
|
230
|
+
});
|
|
231
|
+
} else {
|
|
232
|
+
// Check existing ApiProperty decorators for German descriptions
|
|
233
|
+
member.decorators?.forEach(decorator => {
|
|
234
|
+
const decoratorName = decorator.expression?.name || decorator.expression?.callee?.name;
|
|
235
|
+
if (decoratorName === "ApiProperty" || decoratorName === "ApiPropertyOptional") {
|
|
236
|
+
const propertyName = member.key?.name || "unknown";
|
|
237
|
+
checkApiPropertyDescription(decorator, propertyName);
|
|
238
|
+
}
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// For Entity files
|
|
245
|
+
if (isEntityFile) {
|
|
246
|
+
classProperties.forEach(member => {
|
|
247
|
+
const swaggerDecorators = getSwaggerDecorators(member.decorators);
|
|
248
|
+
if (swaggerDecorators.length > 0) {
|
|
249
|
+
const propertyName = member.key?.name || "unknown";
|
|
250
|
+
|
|
251
|
+
context.report({
|
|
252
|
+
node: member,
|
|
253
|
+
messageId: "forbiddenSwaggerInEntity",
|
|
254
|
+
data: {
|
|
255
|
+
propertyName: propertyName,
|
|
256
|
+
decoratorName: swaggerDecorators.join(", ")
|
|
257
|
+
}
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
};
|
|
@@ -0,0 +1,352 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ESLint-Regel: Prüft korrekte Typ-Übereinstimmung zwischen Entity und DTO Properties
|
|
3
|
+
* Spezifisch für Entity-DTO-Mappings:
|
|
4
|
+
* - ClassTranslationEntity[] -> ClassTranslationEntityDto[]
|
|
5
|
+
* - MonsterEntity[] -> MonsterEntityDto[]
|
|
6
|
+
* - etc.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import fs from "fs";
|
|
10
|
+
import path from "path";
|
|
11
|
+
|
|
12
|
+
/** @type {import('eslint').Rule.RuleModule} */
|
|
13
|
+
const dtoEntityTypeConsistencyRule = {
|
|
14
|
+
meta: {
|
|
15
|
+
type: "problem",
|
|
16
|
+
docs: {
|
|
17
|
+
description: "Entity-DTO Properties müssen korrekte DTO-Typen für Relations haben",
|
|
18
|
+
category: "Architecture",
|
|
19
|
+
recommended: true,
|
|
20
|
+
},
|
|
21
|
+
hasSuggestions: true,
|
|
22
|
+
schema: [],
|
|
23
|
+
messages: {
|
|
24
|
+
incorrectRelationType: "Entity-DTO '{{dtoName}}' Property '{{prop}}' hat falschen Relation-Typ: DTO hat '{{dtoType}}', Entity hat '{{entityType}}'. Erwartet: {{expectedDtoType}}",
|
|
25
|
+
missingRelationDto: "Entity-DTO '{{dtoName}}' Property '{{prop}}' verwendet Entity-Typ '{{entityType}}' statt DTO-Typ '{{expectedDtoType}}'",
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
|
|
29
|
+
create(context) {
|
|
30
|
+
return {
|
|
31
|
+
ClassDeclaration(node) {
|
|
32
|
+
const filename = context.getFilename();
|
|
33
|
+
|
|
34
|
+
// Nur für DTO-Dateien anwenden
|
|
35
|
+
if (!filename.includes('/dto/') && !filename.endsWith('Dto.ts')) {
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const dtoName = node.id.name;
|
|
40
|
+
if (!dtoName) return;
|
|
41
|
+
|
|
42
|
+
// Ausnahme: CreationDataDto-Klassen verwenden Entity-Typen für Entity-Erstellung
|
|
43
|
+
if (dtoName.includes('CreationData') || dtoName.includes('CreateData')) {
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Prüfe DTO-Properties direkt
|
|
48
|
+
node.body.body.forEach(member => {
|
|
49
|
+
if (member.type === 'PropertyDefinition' && member.key.type === 'Identifier') {
|
|
50
|
+
const propName = member.key.name;
|
|
51
|
+
const dtoType = getTypeString(member.typeAnnotation?.typeAnnotation);
|
|
52
|
+
|
|
53
|
+
// Prüfe, ob es sich um eine Entity-Relation handelt (ohne EntityDto)
|
|
54
|
+
// Enums sind erlaubt (z.B. EntityTargetType, MovementPatternType)
|
|
55
|
+
if (isEntityRelationType(dtoType)) {
|
|
56
|
+
const expectedDtoType = getExpectedDtoType(dtoType);
|
|
57
|
+
|
|
58
|
+
context.report({
|
|
59
|
+
node: member,
|
|
60
|
+
messageId: 'missingRelationDto',
|
|
61
|
+
data: {
|
|
62
|
+
dtoName,
|
|
63
|
+
prop: propName,
|
|
64
|
+
dtoType,
|
|
65
|
+
entityType: dtoType,
|
|
66
|
+
expectedDtoType
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
function findEntityFile(dtoName) {
|
|
76
|
+
let entityName = dtoName;
|
|
77
|
+
if (entityName.endsWith("DtoEntityDto")) {
|
|
78
|
+
entityName = entityName.replace("DtoEntityDto", "Entity");
|
|
79
|
+
} else if (entityName.endsWith("EntityDto")) {
|
|
80
|
+
entityName = entityName.replace("EntityDto", "Entity");
|
|
81
|
+
} else if (entityName.endsWith("Dto")) {
|
|
82
|
+
entityName = entityName.replace("Dto", "Entity");
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const currentFile = context.getFilename();
|
|
86
|
+
const srcIndex = currentFile.indexOf('/src/');
|
|
87
|
+
const testIndex = currentFile.indexOf('/__tests__/');
|
|
88
|
+
|
|
89
|
+
if (srcIndex === -1 && testIndex === -1) {
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
let entityDir;
|
|
94
|
+
if (srcIndex !== -1) {
|
|
95
|
+
const srcPath = currentFile.substring(0, srcIndex + 5);
|
|
96
|
+
entityDir = path.resolve(srcPath, "entity");
|
|
97
|
+
} else {
|
|
98
|
+
// Für Test-Fixtures: Suche nach Entity-Dateien in der Nähe
|
|
99
|
+
const testPath = currentFile.substring(0, testIndex);
|
|
100
|
+
entityDir = path.resolve(testPath, "src", "entity");
|
|
101
|
+
|
|
102
|
+
// Fallback: Suche in den Fixtures selbst
|
|
103
|
+
if (!fs.existsSync(entityDir)) {
|
|
104
|
+
entityDir = path.resolve(testPath, "__tests__", "fixtures");
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
try {
|
|
109
|
+
const entityFile = findFileRecursively(entityDir, entityName + ".ts");
|
|
110
|
+
if (entityFile) {
|
|
111
|
+
return entityFile;
|
|
112
|
+
}
|
|
113
|
+
} catch {
|
|
114
|
+
// Entity-Verzeichnis existiert nicht
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return null;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function findFileRecursively(dir, filename) {
|
|
121
|
+
if (!fs.existsSync(dir)) {
|
|
122
|
+
return null;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const files = fs.readdirSync(dir);
|
|
126
|
+
for (const file of files) {
|
|
127
|
+
const filePath = path.join(dir, file);
|
|
128
|
+
const stat = fs.statSync(filePath);
|
|
129
|
+
|
|
130
|
+
if (stat.isDirectory()) {
|
|
131
|
+
const result = findFileRecursively(filePath, filename);
|
|
132
|
+
if (result) {
|
|
133
|
+
return result;
|
|
134
|
+
}
|
|
135
|
+
} else if (file === filename) {
|
|
136
|
+
return filePath;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
return null;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function extractEntityRelations(entityContent) {
|
|
143
|
+
const relations = new Map();
|
|
144
|
+
|
|
145
|
+
// Suche nach @OneToMany, @ManyToOne, @OneToOne, @ManyToMany Dekoratoren
|
|
146
|
+
const relationPatterns = [
|
|
147
|
+
/@OneToMany\s*\(\s*\(\)\s*=>\s*(\w+)/g,
|
|
148
|
+
/@ManyToOne\s*\(\s*\(\)\s*=>\s*(\w+)/g,
|
|
149
|
+
/@OneToOne\s*\(\s*\(\)\s*=>\s*(\w+)/g,
|
|
150
|
+
/@ManyToMany\s*\(\s*\(\)\s*=>\s*(\w+)/g,
|
|
151
|
+
];
|
|
152
|
+
|
|
153
|
+
const lines = entityContent.split('\n');
|
|
154
|
+
|
|
155
|
+
for (let i = 0; i < lines.length; i++) {
|
|
156
|
+
const line = lines[i].trim();
|
|
157
|
+
|
|
158
|
+
// Suche nach Relation-Dekoratoren
|
|
159
|
+
for (const pattern of relationPatterns) {
|
|
160
|
+
const match = pattern.exec(line);
|
|
161
|
+
if (match) {
|
|
162
|
+
const relationEntityName = match[1];
|
|
163
|
+
|
|
164
|
+
// Suche nach der Property-Definition nach dem Dekorator
|
|
165
|
+
let j = i + 1;
|
|
166
|
+
while (j < lines.length && !lines[j].trim().match(/^\w+:\s*[^;]+;$/)) {
|
|
167
|
+
j++;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (j < lines.length) {
|
|
171
|
+
const propLine = lines[j].trim();
|
|
172
|
+
const propMatch = propLine.match(/^(\w+):\s*([^;]+);$/);
|
|
173
|
+
|
|
174
|
+
if (propMatch) {
|
|
175
|
+
const propName = propMatch[1];
|
|
176
|
+
const propType = propMatch[2].trim();
|
|
177
|
+
|
|
178
|
+
relations.set(propName, {
|
|
179
|
+
entityType: relationEntityName,
|
|
180
|
+
fullType: propType
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
return relations;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function extractDtoProperties(classNode) {
|
|
192
|
+
const properties = new Map();
|
|
193
|
+
|
|
194
|
+
classNode.body.body.forEach(member => {
|
|
195
|
+
if (member.type === "PropertyDefinition" && member.key?.name) {
|
|
196
|
+
const propName = member.key.name;
|
|
197
|
+
let propType = "unknown";
|
|
198
|
+
|
|
199
|
+
if (member.typeAnnotation?.typeAnnotation) {
|
|
200
|
+
propType = getTypeString(member.typeAnnotation.typeAnnotation);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
properties.set(propName, propType);
|
|
204
|
+
}
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
return properties;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function getTypeString(typeNode) {
|
|
211
|
+
if (!typeNode) {
|
|
212
|
+
return "unknown";
|
|
213
|
+
}
|
|
214
|
+
switch (typeNode.type) {
|
|
215
|
+
case "TSStringKeyword":
|
|
216
|
+
return "string";
|
|
217
|
+
case "TSNumberKeyword":
|
|
218
|
+
return "number";
|
|
219
|
+
case "TSBooleanKeyword":
|
|
220
|
+
return "boolean";
|
|
221
|
+
case "TSNullKeyword":
|
|
222
|
+
return "null";
|
|
223
|
+
case "TSUndefinedKeyword":
|
|
224
|
+
return "undefined";
|
|
225
|
+
case "TSTypeReference":
|
|
226
|
+
return typeNode.typeName?.name || "unknown";
|
|
227
|
+
case "TSUnionType":
|
|
228
|
+
return typeNode.types.map(getTypeString).join(" | ");
|
|
229
|
+
case "TSArrayType":
|
|
230
|
+
return getTypeString(typeNode.elementType) + "[]";
|
|
231
|
+
case "TSTypeLiteral":
|
|
232
|
+
return "object";
|
|
233
|
+
default:
|
|
234
|
+
return "unknown";
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function isEntityRelationType(type) {
|
|
239
|
+
// Prüfe, ob der Typ eine Entity-Relation ist
|
|
240
|
+
// Enums sind erlaubt (z.B. EntityTargetType, MovementPatternType)
|
|
241
|
+
if (type.includes("Type") || type.includes("Enum")) {
|
|
242
|
+
return false;
|
|
243
|
+
}
|
|
244
|
+
return type.includes("Entity") && !type.includes("EntityDto");
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function getExpectedDtoType(entityType) {
|
|
248
|
+
// Transformiere Entity-Typ zu DTO-Typ
|
|
249
|
+
if (entityType.endsWith("Entity")) {
|
|
250
|
+
return entityType.replace("Entity", "EntityDto");
|
|
251
|
+
}
|
|
252
|
+
return entityType + "Dto";
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function isCorrectDtoType(dtoType, expectedEntityType) {
|
|
256
|
+
const expectedDtoType = getExpectedDtoType(expectedEntityType);
|
|
257
|
+
|
|
258
|
+
// Prüfe verschiedene Varianten
|
|
259
|
+
const variants = [
|
|
260
|
+
expectedDtoType,
|
|
261
|
+
expectedDtoType + "[]",
|
|
262
|
+
expectedDtoType + " | null",
|
|
263
|
+
expectedDtoType + "[] | null"
|
|
264
|
+
];
|
|
265
|
+
|
|
266
|
+
return variants.some(variant => dtoType === variant);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// Speichere DTO-Informationen für spätere Verwendung
|
|
270
|
+
const dtoInfoMap = new Map();
|
|
271
|
+
|
|
272
|
+
return {
|
|
273
|
+
ClassDeclaration(node) {
|
|
274
|
+
const filename = context.getFilename();
|
|
275
|
+
if (!filename.includes("/dto/Entity/")) return;
|
|
276
|
+
|
|
277
|
+
const dtoName = node.id.name;
|
|
278
|
+
const entityFile = findEntityFile(dtoName);
|
|
279
|
+
|
|
280
|
+
if (!entityFile) return;
|
|
281
|
+
|
|
282
|
+
// Parse Entity-Datei und extrahiere Relations
|
|
283
|
+
const entityContent = fs.readFileSync(entityFile, "utf8");
|
|
284
|
+
const entityRelations = extractEntityRelations(entityContent);
|
|
285
|
+
const dtoProperties = extractDtoProperties(node);
|
|
286
|
+
|
|
287
|
+
dtoInfoMap.set(dtoName, {
|
|
288
|
+
entityRelations,
|
|
289
|
+
dtoProperties,
|
|
290
|
+
entityName: path.basename(entityFile, ".ts")
|
|
291
|
+
});
|
|
292
|
+
},
|
|
293
|
+
|
|
294
|
+
NewExpression(node) {
|
|
295
|
+
if (node.callee.type !== "Identifier") return;
|
|
296
|
+
|
|
297
|
+
const dtoName = node.callee.name;
|
|
298
|
+
const dtoInfo = dtoInfoMap.get(dtoName);
|
|
299
|
+
|
|
300
|
+
if (!dtoInfo) return;
|
|
301
|
+
|
|
302
|
+
const { entityRelations, dtoProperties } = dtoInfo;
|
|
303
|
+
|
|
304
|
+
// Prüfe jede Entity-Relation
|
|
305
|
+
for (const [propName, relationInfo] of entityRelations) {
|
|
306
|
+
const dtoType = dtoProperties.get(propName);
|
|
307
|
+
|
|
308
|
+
if (!dtoType) continue;
|
|
309
|
+
|
|
310
|
+
const { entityType } = relationInfo;
|
|
311
|
+
const expectedDtoType = getExpectedDtoType(entityType);
|
|
312
|
+
|
|
313
|
+
// Prüfe, ob DTO-Typ korrekt ist
|
|
314
|
+
if (!isCorrectDtoType(dtoType, entityType)) {
|
|
315
|
+
// Spezielle Behandlung: Wenn DTO-Typ Entity-Typ verwendet statt DTO-Typ
|
|
316
|
+
if (isEntityRelationType(dtoType)) {
|
|
317
|
+
context.report({
|
|
318
|
+
node,
|
|
319
|
+
messageId: "missingRelationDto",
|
|
320
|
+
data: {
|
|
321
|
+
dtoName,
|
|
322
|
+
prop: propName,
|
|
323
|
+
dtoType,
|
|
324
|
+
entityType,
|
|
325
|
+
expectedDtoType
|
|
326
|
+
}
|
|
327
|
+
});
|
|
328
|
+
} else {
|
|
329
|
+
context.report({
|
|
330
|
+
node,
|
|
331
|
+
messageId: "incorrectRelationType",
|
|
332
|
+
data: {
|
|
333
|
+
dtoName,
|
|
334
|
+
prop: propName,
|
|
335
|
+
dtoType,
|
|
336
|
+
entityType,
|
|
337
|
+
expectedDtoType
|
|
338
|
+
}
|
|
339
|
+
});
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
};
|
|
345
|
+
},
|
|
346
|
+
};
|
|
347
|
+
|
|
348
|
+
export default {
|
|
349
|
+
rules: {
|
|
350
|
+
"dto-entity-type-consistency": dtoEntityTypeConsistencyRule,
|
|
351
|
+
},
|
|
352
|
+
};
|