@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,217 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Enforce that Request DTOs have required fields for properties that are required in the corresponding Entity DTO
|
|
3
|
+
* @author Echoes of Order Team
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import fs from "fs";
|
|
7
|
+
import path from "path";
|
|
8
|
+
|
|
9
|
+
/** @type {import('eslint').Rule.RuleModule} */
|
|
10
|
+
const enforceRequestDtoRequiredFieldsRule = {
|
|
11
|
+
meta: {
|
|
12
|
+
type: "problem",
|
|
13
|
+
docs: {
|
|
14
|
+
description: "Request DTOs must not have optional properties if they are required in the corresponding Entity DTO",
|
|
15
|
+
category: "Architecture",
|
|
16
|
+
recommended: true,
|
|
17
|
+
},
|
|
18
|
+
fixable: null,
|
|
19
|
+
schema: [],
|
|
20
|
+
messages: {
|
|
21
|
+
optionalPropertyInRequestDto: "Property '{{propertyName}}' is optional in Request DTO but required in Entity DTO '{{entityDtoName}}'. Make it required: {{propertyName}}!: {{propertyType}}",
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
|
|
25
|
+
create(context) {
|
|
26
|
+
const filename = context.getFilename();
|
|
27
|
+
|
|
28
|
+
// Nur auf Request DTOs anwenden
|
|
29
|
+
if (!filename.includes("/dto/Request/") || !filename.endsWith("RequestDto.ts")) {
|
|
30
|
+
return {};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function getRequestDtoName(filename) {
|
|
34
|
+
return path.basename(filename, ".ts");
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function getCorrespondingEntityDtoPath(filename) {
|
|
38
|
+
const requestDtoName = getRequestDtoName(filename);
|
|
39
|
+
const entityDtoName = requestDtoName.replace(/RequestDto$/, "EntityDto");
|
|
40
|
+
|
|
41
|
+
// Suche nach der Entity DTO im gleichen Verzeichnis oder in /dto/Entity/
|
|
42
|
+
const baseDir = path.dirname(filename);
|
|
43
|
+
const projectRoot = filename.substring(0, filename.indexOf("/dto/"));
|
|
44
|
+
|
|
45
|
+
// Mögliche Pfade für Entity DTOs
|
|
46
|
+
const possiblePaths = [
|
|
47
|
+
path.join(baseDir, `${entityDtoName}.ts`),
|
|
48
|
+
path.join(projectRoot, "dto", "Entity", `${entityDtoName}.ts`),
|
|
49
|
+
// Weitere mögliche Unterverzeichnisse
|
|
50
|
+
...findEntityDtoInSubdirectories(path.join(projectRoot, "dto", "Entity"), entityDtoName),
|
|
51
|
+
];
|
|
52
|
+
|
|
53
|
+
for (const possiblePath of possiblePaths) {
|
|
54
|
+
if (fs.existsSync(possiblePath)) {
|
|
55
|
+
return possiblePath;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function findEntityDtoInSubdirectories(baseDir, entityDtoName) {
|
|
63
|
+
const results = [];
|
|
64
|
+
|
|
65
|
+
if (!fs.existsSync(baseDir)) {
|
|
66
|
+
return results;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function traverse(dir) {
|
|
70
|
+
try {
|
|
71
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
72
|
+
|
|
73
|
+
for (const entry of entries) {
|
|
74
|
+
const fullPath = path.join(dir, entry.name);
|
|
75
|
+
|
|
76
|
+
if (entry.isDirectory()) {
|
|
77
|
+
traverse(fullPath);
|
|
78
|
+
} else if (entry.isFile() && entry.name === `${entityDtoName}.ts`) {
|
|
79
|
+
results.push(fullPath);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
} catch (error) {
|
|
83
|
+
// Ignoriere Fehler beim Durchsuchen
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
traverse(baseDir);
|
|
88
|
+
return results;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function getEntityDtoProperties(entityDtoPath) {
|
|
92
|
+
try {
|
|
93
|
+
const content = fs.readFileSync(entityDtoPath, "utf-8");
|
|
94
|
+
const lines = content.split("\n");
|
|
95
|
+
const properties = {};
|
|
96
|
+
|
|
97
|
+
let inClassBody = false;
|
|
98
|
+
const AUTO_MANAGED_FIELDS = ["id", "createdAt", "updatedAt"];
|
|
99
|
+
|
|
100
|
+
for (const line of lines) {
|
|
101
|
+
if (line.includes("export default class") || line.includes("export class")) {
|
|
102
|
+
inClassBody = true;
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (inClassBody && line.trim() === "}") {
|
|
107
|
+
break;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (inClassBody && !line.includes("static ") && !line.trim().startsWith("@")) {
|
|
111
|
+
const propertyMatch = line.match(/^\s+(\w+)(\?)?:\s+([^;]+);/);
|
|
112
|
+
if (propertyMatch) {
|
|
113
|
+
const propertyName = propertyMatch[1];
|
|
114
|
+
const isOptional = propertyMatch[2] === "?";
|
|
115
|
+
const propertyType = propertyMatch[3].trim();
|
|
116
|
+
|
|
117
|
+
// Ignoriere auto-managed fields
|
|
118
|
+
if (!AUTO_MANAGED_FIELDS.includes(propertyName) && !propertyName.startsWith("_")) {
|
|
119
|
+
properties[propertyName] = {
|
|
120
|
+
isOptional,
|
|
121
|
+
type: propertyType,
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return properties;
|
|
129
|
+
} catch (error) {
|
|
130
|
+
return {};
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function getRequestDtoProperties(classNode) {
|
|
135
|
+
const properties = {};
|
|
136
|
+
|
|
137
|
+
if (!classNode || !classNode.body) {
|
|
138
|
+
return properties;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
for (const member of classNode.body.body) {
|
|
142
|
+
if (member.type === "PropertyDefinition" || member.type === "ClassProperty") {
|
|
143
|
+
const key = member.key;
|
|
144
|
+
if (key && key.type === "Identifier") {
|
|
145
|
+
const propertyName = key.name;
|
|
146
|
+
const isOptional = member.optional === true;
|
|
147
|
+
|
|
148
|
+
let propertyType = "unknown";
|
|
149
|
+
if (member.typeAnnotation && member.typeAnnotation.typeAnnotation) {
|
|
150
|
+
const typeNode = member.typeAnnotation.typeAnnotation;
|
|
151
|
+
if (typeNode.type === "TSTypeReference" && typeNode.typeName) {
|
|
152
|
+
propertyType = typeNode.typeName.name;
|
|
153
|
+
} else if (typeNode.type === "TSStringKeyword") {
|
|
154
|
+
propertyType = "string";
|
|
155
|
+
} else if (typeNode.type === "TSNumberKeyword") {
|
|
156
|
+
propertyType = "number";
|
|
157
|
+
} else if (typeNode.type === "TSBooleanKeyword") {
|
|
158
|
+
propertyType = "boolean";
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (!propertyName.startsWith("_") && propertyName !== "constructor") {
|
|
163
|
+
properties[propertyName] = {
|
|
164
|
+
isOptional,
|
|
165
|
+
type: propertyType,
|
|
166
|
+
node: member,
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return properties;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return {
|
|
177
|
+
ClassDeclaration(node) {
|
|
178
|
+
const entityDtoPath = getCorrespondingEntityDtoPath(filename);
|
|
179
|
+
|
|
180
|
+
if (!entityDtoPath) {
|
|
181
|
+
// Keine korrespondierende Entity DTO gefunden - kein Fehler
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const entityDtoProperties = getEntityDtoProperties(entityDtoPath);
|
|
186
|
+
const requestDtoProperties = getRequestDtoProperties(node);
|
|
187
|
+
|
|
188
|
+
// Vergleiche Properties
|
|
189
|
+
for (const [propertyName, requestProp] of Object.entries(requestDtoProperties)) {
|
|
190
|
+
const entityProp = entityDtoProperties[propertyName];
|
|
191
|
+
|
|
192
|
+
if (entityProp) {
|
|
193
|
+
// Property existiert in beiden DTOs
|
|
194
|
+
if (requestProp.isOptional && !entityProp.isOptional) {
|
|
195
|
+
// Property ist im Request DTO optional, aber in Entity DTO required
|
|
196
|
+
context.report({
|
|
197
|
+
node: requestProp.node,
|
|
198
|
+
messageId: "optionalPropertyInRequestDto",
|
|
199
|
+
data: {
|
|
200
|
+
propertyName,
|
|
201
|
+
entityDtoName: getRequestDtoName(filename).replace(/RequestDto$/, "EntityDto"),
|
|
202
|
+
propertyType: requestProp.type,
|
|
203
|
+
},
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
},
|
|
209
|
+
};
|
|
210
|
+
},
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
export default {
|
|
214
|
+
rules: {
|
|
215
|
+
"enforce-request-dto-required-fields": enforceRequestDtoRequiredFieldsRule,
|
|
216
|
+
},
|
|
217
|
+
};
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
export default {
|
|
2
|
+
meta: {
|
|
3
|
+
type: "suggestion",
|
|
4
|
+
docs: {
|
|
5
|
+
description: "Enforce consistent Result pattern usage",
|
|
6
|
+
category: "Best Practices",
|
|
7
|
+
recommended: true,
|
|
8
|
+
},
|
|
9
|
+
fixable: null,
|
|
10
|
+
schema: [],
|
|
11
|
+
},
|
|
12
|
+
create(context) {
|
|
13
|
+
return {
|
|
14
|
+
ImportDeclaration(node) {
|
|
15
|
+
if (node.source.value === "@/dto/Result") {
|
|
16
|
+
const specifiers = node.specifiers;
|
|
17
|
+
const hasDefaultImport = specifiers.some(spec => spec.type === "ImportDefaultSpecifier");
|
|
18
|
+
const hasNamedImport = specifiers.some(spec => spec.type === "ImportSpecifier" && spec.imported.name === "Result");
|
|
19
|
+
|
|
20
|
+
if (hasDefaultImport && hasNamedImport) {
|
|
21
|
+
context.report({
|
|
22
|
+
node,
|
|
23
|
+
message: "Duplicate Result import detected. Use either default or named import, not both.",
|
|
24
|
+
fix(fixer) {
|
|
25
|
+
return fixer.replaceText(node, 'import { Result } from "@/dto/Result";');
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
|
|
32
|
+
CallExpression(node) {
|
|
33
|
+
if (node.callee.type === "Identifier" && node.callee.name === "Result") {
|
|
34
|
+
const args = node.arguments;
|
|
35
|
+
if (args.length === 0) {
|
|
36
|
+
context.report({
|
|
37
|
+
node,
|
|
38
|
+
message: "Result constructor requires at least one argument."
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
};
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
export default {
|
|
2
|
+
meta: {
|
|
3
|
+
type: "problem",
|
|
4
|
+
docs: {
|
|
5
|
+
description: "Enforce services load relations with load*EntityDto() methods",
|
|
6
|
+
category: "Best Practices",
|
|
7
|
+
recommended: true,
|
|
8
|
+
},
|
|
9
|
+
messages: {
|
|
10
|
+
useLoadEntityDtoMethods: "Relations müssen mit private async load*EntityDto() Methoden geladen werden. Pattern: loadQualityEntityDto(), loadBindingEntityDto(), etc.",
|
|
11
|
+
convertEntityToDto: "Entities müssen mit EntityDto.fromEntity() zu EntityDto konvertiert werden. Direkte Entity-Zuweisung ohne DTO-Konvertierung ist nicht erlaubt.",
|
|
12
|
+
missingLoadMethod: "Service sollte eine private async load{{relationName}}EntityDto() Methode haben für das Laden von {{relationName}} Relations.",
|
|
13
|
+
},
|
|
14
|
+
schema: [],
|
|
15
|
+
},
|
|
16
|
+
create (context) {
|
|
17
|
+
const filename = context.getFilename();
|
|
18
|
+
|
|
19
|
+
if (!filename.includes("/service/") && !filename.endsWith("Service.ts")) {
|
|
20
|
+
return {};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
let hasFromRequestDtoCall = false;
|
|
24
|
+
const relationAssignments = [];
|
|
25
|
+
const loadEntityDtoVariables = new Set();
|
|
26
|
+
|
|
27
|
+
return {
|
|
28
|
+
CallExpression (node) {
|
|
29
|
+
if (
|
|
30
|
+
node.callee.type === "MemberExpression" &&
|
|
31
|
+
node.callee.property.name === "fromRequestDto" &&
|
|
32
|
+
node.callee.object.name?.endsWith("EntityDto")
|
|
33
|
+
) {
|
|
34
|
+
hasFromRequestDtoCall = true;
|
|
35
|
+
}
|
|
36
|
+
},
|
|
37
|
+
VariableDeclarator (node) {
|
|
38
|
+
if (
|
|
39
|
+
node.init &&
|
|
40
|
+
node.init.type === "AwaitExpression" &&
|
|
41
|
+
node.init.argument &&
|
|
42
|
+
node.init.argument.type === "CallExpression" &&
|
|
43
|
+
node.init.argument.callee.type === "MemberExpression" &&
|
|
44
|
+
node.init.argument.callee.property.name.startsWith("load") &&
|
|
45
|
+
node.init.argument.callee.property.name.endsWith("EntityDto")
|
|
46
|
+
) {
|
|
47
|
+
if (node.id.type === "Identifier") {
|
|
48
|
+
loadEntityDtoVariables.add(node.id.name);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
},
|
|
52
|
+
ArrayPattern (node) {
|
|
53
|
+
const parent = node.parent;
|
|
54
|
+
if (
|
|
55
|
+
parent &&
|
|
56
|
+
parent.type === "VariableDeclarator" &&
|
|
57
|
+
parent.init &&
|
|
58
|
+
parent.init.type === "AwaitExpression" &&
|
|
59
|
+
parent.init.argument &&
|
|
60
|
+
parent.init.argument.type === "CallExpression" &&
|
|
61
|
+
parent.init.argument.callee.type === "MemberExpression" &&
|
|
62
|
+
parent.init.argument.callee.object.name === "Promise" &&
|
|
63
|
+
parent.init.argument.callee.property.name === "all"
|
|
64
|
+
) {
|
|
65
|
+
node.elements.forEach((element) => {
|
|
66
|
+
if (element && element.type === "Identifier") {
|
|
67
|
+
loadEntityDtoVariables.add(element.name);
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
},
|
|
72
|
+
AssignmentExpression (node) {
|
|
73
|
+
if (
|
|
74
|
+
hasFromRequestDtoCall &&
|
|
75
|
+
node.left.type === "MemberExpression" &&
|
|
76
|
+
node.left.object.name?.toLowerCase().includes("dto")
|
|
77
|
+
) {
|
|
78
|
+
const propertyName = node.left.property.name;
|
|
79
|
+
const rightValue = node.right;
|
|
80
|
+
|
|
81
|
+
const relationKeywords = [
|
|
82
|
+
"quality",
|
|
83
|
+
"binding",
|
|
84
|
+
"itemClass",
|
|
85
|
+
"itemSubclass",
|
|
86
|
+
"inventoryType",
|
|
87
|
+
"type",
|
|
88
|
+
"parent",
|
|
89
|
+
"child",
|
|
90
|
+
];
|
|
91
|
+
|
|
92
|
+
if (relationKeywords.some((keyword) => propertyName.toLowerCase().includes(keyword))) {
|
|
93
|
+
if (
|
|
94
|
+
rightValue.type === "Identifier" &&
|
|
95
|
+
!loadEntityDtoVariables.has(rightValue.name)
|
|
96
|
+
) {
|
|
97
|
+
relationAssignments.push({
|
|
98
|
+
node,
|
|
99
|
+
propertyName,
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
},
|
|
105
|
+
"Program:exit" () {
|
|
106
|
+
for (const { node } of relationAssignments) {
|
|
107
|
+
context.report({
|
|
108
|
+
messageId: "convertEntityToDto",
|
|
109
|
+
node,
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
},
|
|
113
|
+
};
|
|
114
|
+
},
|
|
115
|
+
};
|
|
116
|
+
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ESLint-Regel: enforce-test-coverage
|
|
3
|
+
* Stellt sicher, dass alle Services und Controller entsprechende Test-Dateien haben
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import fs from "fs";
|
|
7
|
+
import path from "path";
|
|
8
|
+
|
|
9
|
+
/** @type {import('eslint').Rule.RuleModule} */
|
|
10
|
+
const enforceTestCoverageRule = {
|
|
11
|
+
meta: {
|
|
12
|
+
type: "problem",
|
|
13
|
+
docs: {
|
|
14
|
+
description: "Alle Services und Controller müssen entsprechende Test-Dateien haben",
|
|
15
|
+
category: "Testing",
|
|
16
|
+
recommended: true,
|
|
17
|
+
},
|
|
18
|
+
schema: [],
|
|
19
|
+
messages: {
|
|
20
|
+
missingTestFile: "{{fileType}} '{{className}}' hat keine entsprechende Test-Datei. Erwarte: {{expectedTestPath}}",
|
|
21
|
+
testFileExists: "Test-Datei gefunden: {{testPath}}",
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
create(context) {
|
|
25
|
+
const filename = context.getFilename();
|
|
26
|
+
|
|
27
|
+
// Nur Services und Controller prüfen
|
|
28
|
+
const isServiceFile = filename.includes("/service/") && filename.endsWith(".ts") && !filename.includes("test");
|
|
29
|
+
const isControllerFile = filename.includes("/controller/") && filename.endsWith("Controller.ts") && !filename.includes("test");
|
|
30
|
+
|
|
31
|
+
if (!isServiceFile && !isControllerFile) return {};
|
|
32
|
+
|
|
33
|
+
function findTestFile(originalFilePath) {
|
|
34
|
+
const dir = path.dirname(originalFilePath);
|
|
35
|
+
const baseName = path.basename(originalFilePath, ".ts");
|
|
36
|
+
|
|
37
|
+
// Mögliche Test-Datei-Patterns
|
|
38
|
+
const testPatterns = [
|
|
39
|
+
path.join(dir, "__tests__", `${baseName}.test.ts`),
|
|
40
|
+
path.join(dir, "__tests__", `${baseName}.spec.ts`),
|
|
41
|
+
path.join(dir, `${baseName}.test.ts`),
|
|
42
|
+
path.join(dir, `${baseName}.spec.ts`),
|
|
43
|
+
];
|
|
44
|
+
|
|
45
|
+
for (const testPath of testPatterns) {
|
|
46
|
+
try {
|
|
47
|
+
if (fs.existsSync(testPath)) {
|
|
48
|
+
return testPath;
|
|
49
|
+
}
|
|
50
|
+
} catch (error) {
|
|
51
|
+
// Datei nicht gefunden, weiter suchen
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return {
|
|
59
|
+
ClassDeclaration(node) {
|
|
60
|
+
const className = node.id?.name;
|
|
61
|
+
if (!className) return;
|
|
62
|
+
|
|
63
|
+
// Bestimme Dateityp
|
|
64
|
+
let fileType = "Class";
|
|
65
|
+
if (className.endsWith("Service")) {
|
|
66
|
+
fileType = "Service";
|
|
67
|
+
} else if (className.endsWith("Controller")) {
|
|
68
|
+
fileType = "Controller";
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Nur Services und Controller prüfen
|
|
72
|
+
if (fileType === "Class") return;
|
|
73
|
+
|
|
74
|
+
const testFile = findTestFile(filename);
|
|
75
|
+
|
|
76
|
+
if (!testFile) {
|
|
77
|
+
const dir = path.dirname(filename);
|
|
78
|
+
const baseName = path.basename(filename, ".ts");
|
|
79
|
+
const expectedTestPath = path.join(dir, "__tests__", `${baseName}.test.ts`);
|
|
80
|
+
|
|
81
|
+
context.report({
|
|
82
|
+
node,
|
|
83
|
+
messageId: "missingTestFile",
|
|
84
|
+
data: {
|
|
85
|
+
fileType,
|
|
86
|
+
className,
|
|
87
|
+
expectedTestPath: path.relative(process.cwd(), expectedTestPath),
|
|
88
|
+
},
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
},
|
|
92
|
+
};
|
|
93
|
+
},
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
export default enforceTestCoverageRule;
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Enforce conditional assignment for id, createdAt, updatedAt in toEntity methods
|
|
3
|
+
* @author Echoes of Order Team
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/** @type {import('eslint').Rule.RuleModule} */
|
|
7
|
+
const enforceToEntityConditionalAssignmentRule = {
|
|
8
|
+
meta: {
|
|
9
|
+
type: "problem",
|
|
10
|
+
docs: {
|
|
11
|
+
description: "id, createdAt, and updatedAt must be set conditionally in toEntity methods (if (dto.field) { entity.field = dto.field; })",
|
|
12
|
+
category: "Architecture",
|
|
13
|
+
recommended: true,
|
|
14
|
+
},
|
|
15
|
+
fixable: "code",
|
|
16
|
+
schema: [],
|
|
17
|
+
messages: {
|
|
18
|
+
mustUseConditional: "Property '{{fieldName}}' in toEntity() must be set conditionally: if (dto.{{fieldName}}) { entity.{{fieldName}} = dto.{{fieldName}}; }",
|
|
19
|
+
},
|
|
20
|
+
},
|
|
21
|
+
create(context) {
|
|
22
|
+
const AUTO_MANAGED_FIELDS = ["id", "createdAt", "updatedAt"];
|
|
23
|
+
|
|
24
|
+
function isToEntityMethod(node) {
|
|
25
|
+
return (
|
|
26
|
+
(node.type === "MethodDefinition" || node.type === "Property") &&
|
|
27
|
+
node.key?.name === "toEntity" &&
|
|
28
|
+
node.static === true
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function isDirectAssignment(node, fieldName) {
|
|
33
|
+
if (
|
|
34
|
+
node.type === "ExpressionStatement" &&
|
|
35
|
+
node.expression?.type === "AssignmentExpression"
|
|
36
|
+
) {
|
|
37
|
+
const left = node.expression.left;
|
|
38
|
+
const right = node.expression.right;
|
|
39
|
+
|
|
40
|
+
if (
|
|
41
|
+
left?.type === "MemberExpression" &&
|
|
42
|
+
left.object?.name === "entity" &&
|
|
43
|
+
left.property?.name === fieldName &&
|
|
44
|
+
right?.type === "MemberExpression" &&
|
|
45
|
+
right.object?.name === "dto" &&
|
|
46
|
+
right.property?.name === fieldName
|
|
47
|
+
) {
|
|
48
|
+
return true;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function isConditionalAssignment(node, fieldName) {
|
|
55
|
+
if (node.type === "IfStatement") {
|
|
56
|
+
const test = node.test;
|
|
57
|
+
if (
|
|
58
|
+
test?.type === "MemberExpression" &&
|
|
59
|
+
test.object?.name === "dto" &&
|
|
60
|
+
test.property?.name === fieldName
|
|
61
|
+
) {
|
|
62
|
+
const body = node.consequent?.body || [node.consequent];
|
|
63
|
+
const hasCorrectAssignment = body.some((stmt) => {
|
|
64
|
+
if (
|
|
65
|
+
stmt.type === "ExpressionStatement" &&
|
|
66
|
+
stmt.expression?.type === "AssignmentExpression"
|
|
67
|
+
) {
|
|
68
|
+
const left = stmt.expression.left;
|
|
69
|
+
const right = stmt.expression.right;
|
|
70
|
+
return (
|
|
71
|
+
left?.type === "MemberExpression" &&
|
|
72
|
+
left.object?.name === "entity" &&
|
|
73
|
+
left.property?.name === fieldName &&
|
|
74
|
+
right?.type === "MemberExpression" &&
|
|
75
|
+
right.object?.name === "dto" &&
|
|
76
|
+
right.property?.name === fieldName
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
return false;
|
|
80
|
+
});
|
|
81
|
+
return hasCorrectAssignment;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return {
|
|
88
|
+
MethodDefinition(node) {
|
|
89
|
+
if (!isToEntityMethod(node)) {
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const body = node.value?.body?.body;
|
|
94
|
+
if (!body || !Array.isArray(body)) {
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
AUTO_MANAGED_FIELDS.forEach((fieldName) => {
|
|
99
|
+
let hasDirectAssignment = false;
|
|
100
|
+
let hasConditionalAssignment = false;
|
|
101
|
+
|
|
102
|
+
body.forEach((statement) => {
|
|
103
|
+
if (isDirectAssignment(statement, fieldName)) {
|
|
104
|
+
hasDirectAssignment = true;
|
|
105
|
+
context.report({
|
|
106
|
+
node: statement,
|
|
107
|
+
messageId: "mustUseConditional",
|
|
108
|
+
data: { fieldName },
|
|
109
|
+
fix(fixer) {
|
|
110
|
+
const assignmentText = `if (dto.${fieldName}) {\n entity.${fieldName} = dto.${fieldName};\n }`;
|
|
111
|
+
return fixer.replaceText(statement, assignmentText);
|
|
112
|
+
},
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (isConditionalAssignment(statement, fieldName)) {
|
|
117
|
+
hasConditionalAssignment = true;
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
},
|
|
122
|
+
};
|
|
123
|
+
},
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
export default {
|
|
127
|
+
rules: {
|
|
128
|
+
"enforce-toentity-conditional-assignment": enforceToEntityConditionalAssignmentRule,
|
|
129
|
+
},
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
|