@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,159 @@
|
|
|
1
|
+
export default {
|
|
2
|
+
meta: {
|
|
3
|
+
type: "problem",
|
|
4
|
+
docs: {
|
|
5
|
+
description: "Enforce no explicit visibility modifiers or readonly in DTO classes",
|
|
6
|
+
category: "Best Practices",
|
|
7
|
+
recommended: true,
|
|
8
|
+
},
|
|
9
|
+
fixable: "code",
|
|
10
|
+
schema: [],
|
|
11
|
+
messages: {
|
|
12
|
+
removePublicModifier: "Remove unnecessary 'public' modifier from DTO member '{{memberName}}'",
|
|
13
|
+
removePrivateModifier: "Remove 'private' modifier from DTO member '{{memberName}}' - DTO members should be accessible",
|
|
14
|
+
removeProtectedModifier: "Remove 'protected' modifier from DTO member '{{memberName}}' - DTO members should be accessible",
|
|
15
|
+
removeReadonlyModifier: "Remove 'readonly' modifier from DTO member '{{memberName}}' - DTO members should be mutable",
|
|
16
|
+
},
|
|
17
|
+
},
|
|
18
|
+
|
|
19
|
+
create (context) {
|
|
20
|
+
return {
|
|
21
|
+
PropertyDefinition (node) {
|
|
22
|
+
// Check if this property is in a DTO class
|
|
23
|
+
const parent = node.parent;
|
|
24
|
+
if (parent && parent.type === "ClassBody") {
|
|
25
|
+
const classNode = parent.parent;
|
|
26
|
+
if (classNode && classNode.type === "ClassDeclaration") {
|
|
27
|
+
const className = classNode.id?.name;
|
|
28
|
+
if (className && className.endsWith("Dto")) {
|
|
29
|
+
// This is a DTO class property
|
|
30
|
+
|
|
31
|
+
// Check for visibility modifiers
|
|
32
|
+
if (node.accessibility) {
|
|
33
|
+
const memberName = node.key?.name || "unknown";
|
|
34
|
+
const modifier = node.accessibility;
|
|
35
|
+
|
|
36
|
+
context.report({
|
|
37
|
+
node,
|
|
38
|
+
messageId: `remove${modifier.charAt(0).toUpperCase() + modifier.slice(1)}Modifier`,
|
|
39
|
+
data: {
|
|
40
|
+
memberName,
|
|
41
|
+
},
|
|
42
|
+
fix (fixer) {
|
|
43
|
+
const sourceCode = context.getSourceCode();
|
|
44
|
+
const tokens = sourceCode.getTokens(node);
|
|
45
|
+
|
|
46
|
+
// Find the visibility modifier token
|
|
47
|
+
for (const token of tokens) {
|
|
48
|
+
if (token.type === "Keyword" &&
|
|
49
|
+
(token.value === "public" || token.value === "private" || token.value === "protected")) {
|
|
50
|
+
|
|
51
|
+
// Find the next token after the modifier
|
|
52
|
+
const nextToken = sourceCode.getTokenAfter(token);
|
|
53
|
+
|
|
54
|
+
if (nextToken) {
|
|
55
|
+
// Remove from start of modifier to start of next token (includes whitespace)
|
|
56
|
+
return fixer.removeRange([token.range[0], nextToken.range[0]]);
|
|
57
|
+
} else {
|
|
58
|
+
// Just remove the modifier token
|
|
59
|
+
return fixer.remove(token);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return null;
|
|
65
|
+
},
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Check for readonly modifier
|
|
70
|
+
if (node.readonly) {
|
|
71
|
+
const memberName = node.key?.name || "unknown";
|
|
72
|
+
|
|
73
|
+
context.report({
|
|
74
|
+
node,
|
|
75
|
+
messageId: "removeReadonlyModifier",
|
|
76
|
+
data: {
|
|
77
|
+
memberName,
|
|
78
|
+
},
|
|
79
|
+
fix (fixer) {
|
|
80
|
+
const sourceCode = context.getSourceCode();
|
|
81
|
+
const tokens = sourceCode.getTokens(node);
|
|
82
|
+
|
|
83
|
+
// Find the readonly modifier token
|
|
84
|
+
for (const token of tokens) {
|
|
85
|
+
if (token.type === "Keyword" && token.value === "readonly") {
|
|
86
|
+
// Find the next token after the modifier
|
|
87
|
+
const nextToken = sourceCode.getTokenAfter(token);
|
|
88
|
+
|
|
89
|
+
if (nextToken) {
|
|
90
|
+
// Remove from start of modifier to start of next token (includes whitespace)
|
|
91
|
+
return fixer.removeRange([token.range[0], nextToken.range[0]]);
|
|
92
|
+
} else {
|
|
93
|
+
// Just remove the modifier token
|
|
94
|
+
return fixer.remove(token);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return null;
|
|
100
|
+
},
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
},
|
|
107
|
+
|
|
108
|
+
MethodDefinition (node) {
|
|
109
|
+
// Check if this method is in a DTO class
|
|
110
|
+
const parent = node.parent;
|
|
111
|
+
if (parent && parent.type === "ClassBody") {
|
|
112
|
+
const classNode = parent.parent;
|
|
113
|
+
if (classNode && classNode.type === "ClassDeclaration") {
|
|
114
|
+
const className = classNode.id?.name;
|
|
115
|
+
if (className && className.endsWith("Dto")) {
|
|
116
|
+
// This is a DTO class method
|
|
117
|
+
if (node.accessibility) {
|
|
118
|
+
const memberName = node.key?.name || "unknown";
|
|
119
|
+
const modifier = node.accessibility;
|
|
120
|
+
|
|
121
|
+
context.report({
|
|
122
|
+
node,
|
|
123
|
+
messageId: `remove${modifier.charAt(0).toUpperCase() + modifier.slice(1)}Modifier`,
|
|
124
|
+
data: {
|
|
125
|
+
memberName,
|
|
126
|
+
},
|
|
127
|
+
fix (fixer) {
|
|
128
|
+
const sourceCode = context.getSourceCode();
|
|
129
|
+
const tokens = sourceCode.getTokens(node);
|
|
130
|
+
|
|
131
|
+
// Find the visibility modifier token
|
|
132
|
+
for (const token of tokens) {
|
|
133
|
+
if (token.type === "Keyword" &&
|
|
134
|
+
(token.value === "public" || token.value === "private" || token.value === "protected")) {
|
|
135
|
+
|
|
136
|
+
// Find the next token after the modifier
|
|
137
|
+
const nextToken = sourceCode.getTokenAfter(token);
|
|
138
|
+
|
|
139
|
+
if (nextToken) {
|
|
140
|
+
// Remove from start of modifier to start of next token (includes whitespace)
|
|
141
|
+
return fixer.removeRange([token.range[0], nextToken.range[0]]);
|
|
142
|
+
} else {
|
|
143
|
+
// Just remove the modifier token
|
|
144
|
+
return fixer.remove(token);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return null;
|
|
150
|
+
},
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
},
|
|
157
|
+
};
|
|
158
|
+
},
|
|
159
|
+
};
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ESLint-Regel: enforce-api-versioning
|
|
3
|
+
* Erzwingt einheitliche API-Versionierung in Controller-Routen
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/** @type {import('eslint').Rule.RuleModule} */
|
|
7
|
+
const enforceApiVersioningRule = {
|
|
8
|
+
meta: {
|
|
9
|
+
type: "problem",
|
|
10
|
+
docs: {
|
|
11
|
+
description: "Erzwingt einheitliche API-Versionierung (/api/v1/) in allen Controller-Routen",
|
|
12
|
+
category: "API Standards",
|
|
13
|
+
recommended: true,
|
|
14
|
+
},
|
|
15
|
+
schema: [],
|
|
16
|
+
messages: {
|
|
17
|
+
missingApiVersion: "Controller-Route '{{route}}' muss API-Versionierung verwenden. Erwarte '/api/v1/' Prefix.",
|
|
18
|
+
inconsistentVersion: "Controller-Route '{{route}}' verwendet inkonsistente Versionierung. Verwende '/api/v1/' für alle Routen.",
|
|
19
|
+
invalidRouteFormat: "Controller-Route '{{route}}' hat ungültiges Format. Erwarte '/api/v1/resource-name'.",
|
|
20
|
+
},
|
|
21
|
+
},
|
|
22
|
+
create(context) {
|
|
23
|
+
const filename = context.getFilename();
|
|
24
|
+
const isControllerFile = filename.includes("/controller/") && filename.endsWith("Controller.ts");
|
|
25
|
+
const isAliasController = filename.includes("AliasController.ts");
|
|
26
|
+
|
|
27
|
+
if (!isControllerFile || isAliasController) return {};
|
|
28
|
+
|
|
29
|
+
const API_VERSION_PATTERN = /^\/api\/v1\/?.*/;
|
|
30
|
+
const VALID_ROUTE_PATTERN = /^\/api\/v1\/?[a-z-]*[a-z0-9-]*(?:\/[a-z-]+[a-z0-9-]*)*$/;
|
|
31
|
+
|
|
32
|
+
function validateControllerRoute(routePath) {
|
|
33
|
+
if (!routePath) return { valid: false, reason: "empty" };
|
|
34
|
+
|
|
35
|
+
// Entferne führende/trailing Whitespace
|
|
36
|
+
const cleanPath = routePath.trim();
|
|
37
|
+
|
|
38
|
+
// Prüfe auf API-Versionierung
|
|
39
|
+
if (!API_VERSION_PATTERN.test(cleanPath)) {
|
|
40
|
+
return { valid: false, reason: "missing-version" };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Prüfe auf korrektes Format
|
|
44
|
+
if (!VALID_ROUTE_PATTERN.test(cleanPath)) {
|
|
45
|
+
return { valid: false, reason: "invalid-format" };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return { valid: true };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return {
|
|
52
|
+
Decorator(node) {
|
|
53
|
+
// Prüfe @Controller() Decorator
|
|
54
|
+
if (node.expression?.name === "Controller" ||
|
|
55
|
+
(node.expression?.type === "CallExpression" &&
|
|
56
|
+
node.expression.callee?.name === "Controller")) {
|
|
57
|
+
|
|
58
|
+
const args = node.expression?.arguments;
|
|
59
|
+
if (args && args.length > 0 && args[0].type === "Literal") {
|
|
60
|
+
const routePath = args[0].value;
|
|
61
|
+
const validation = validateControllerRoute(routePath);
|
|
62
|
+
|
|
63
|
+
if (!validation.valid) {
|
|
64
|
+
let messageId, data;
|
|
65
|
+
|
|
66
|
+
switch (validation.reason) {
|
|
67
|
+
case "missing-version":
|
|
68
|
+
messageId = "missingApiVersion";
|
|
69
|
+
data = { route: routePath };
|
|
70
|
+
break;
|
|
71
|
+
case "invalid-format":
|
|
72
|
+
messageId = "invalidRouteFormat";
|
|
73
|
+
data = { route: routePath };
|
|
74
|
+
break;
|
|
75
|
+
default:
|
|
76
|
+
messageId = "missingApiVersion";
|
|
77
|
+
data = { route: routePath || "undefined" };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
context.report({
|
|
81
|
+
node: args[0],
|
|
82
|
+
messageId,
|
|
83
|
+
data,
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Prüfe HTTP-Method Decoratoren (@Get, @Post, etc.)
|
|
90
|
+
const httpMethods = ["Get", "Post", "Put", "Patch", "Delete"];
|
|
91
|
+
const decoratorName = node.expression?.name || node.expression?.callee?.name;
|
|
92
|
+
|
|
93
|
+
if (httpMethods.includes(decoratorName)) {
|
|
94
|
+
const args = node.expression?.arguments;
|
|
95
|
+
if (args && args.length > 0 && args[0].type === "Literal") {
|
|
96
|
+
const routePath = args[0].value;
|
|
97
|
+
|
|
98
|
+
// Relative Routen sind erlaubt (werden mit Controller-Route kombiniert)
|
|
99
|
+
if (!routePath.startsWith("/")) {
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const validation = validateControllerRoute(routePath);
|
|
104
|
+
if (!validation.valid && validation.reason === "missing-version") {
|
|
105
|
+
context.report({
|
|
106
|
+
node: args[0],
|
|
107
|
+
messageId: "missingApiVersion",
|
|
108
|
+
data: { route: routePath },
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
},
|
|
114
|
+
};
|
|
115
|
+
},
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
export default {
|
|
119
|
+
rules: {
|
|
120
|
+
"enforce-api-versioning": enforceApiVersioningRule,
|
|
121
|
+
},
|
|
122
|
+
};
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ESLint-Regel: enforce-app-module-registration
|
|
3
|
+
*
|
|
4
|
+
* Überprüft, ob alle Controller und Services korrekt im AppModule registriert sind.
|
|
5
|
+
* Verhindert Dependency Injection Fehler durch fehlende Registrierungen.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export default {
|
|
9
|
+
meta: {
|
|
10
|
+
type: "problem",
|
|
11
|
+
docs: {
|
|
12
|
+
description: "Enforce proper registration of controllers and services in AppModule",
|
|
13
|
+
category: "Architecture",
|
|
14
|
+
recommended: true,
|
|
15
|
+
},
|
|
16
|
+
messages: {
|
|
17
|
+
missingController: "Controller '{{controllerName}}' is not registered in AppModule controllers array",
|
|
18
|
+
missingService: "Service '{{serviceName}}' is not registered in AppModule providers array",
|
|
19
|
+
invalidControllerImport: "Controller '{{controllerName}}' is imported but not used in controllers array",
|
|
20
|
+
invalidServiceImport: "Service '{{serviceName}}' is imported but not used in providers array",
|
|
21
|
+
},
|
|
22
|
+
fixable: "code",
|
|
23
|
+
},
|
|
24
|
+
|
|
25
|
+
create(context) {
|
|
26
|
+
const filename = context.getFilename();
|
|
27
|
+
|
|
28
|
+
// Nur für AppModule.ts ausführen
|
|
29
|
+
if (!filename.includes('AppModule.ts')) {
|
|
30
|
+
return {};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return {
|
|
34
|
+
Program(node) {
|
|
35
|
+
try {
|
|
36
|
+
// Analysiere AppModule AST
|
|
37
|
+
const appModuleAnalysis = analyzeAppModule(node);
|
|
38
|
+
|
|
39
|
+
// Prüfe fehlende Controller
|
|
40
|
+
appModuleAnalysis.missingControllers.forEach(controller => {
|
|
41
|
+
context.report({
|
|
42
|
+
node,
|
|
43
|
+
messageId: "missingController",
|
|
44
|
+
data: { controllerName: controller.name }
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
// Prüfe fehlende Services
|
|
49
|
+
appModuleAnalysis.missingServices.forEach(service => {
|
|
50
|
+
context.report({
|
|
51
|
+
node,
|
|
52
|
+
messageId: "missingService",
|
|
53
|
+
data: { serviceName: service.name }
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
// Prüfe ungenutzte Imports
|
|
58
|
+
appModuleAnalysis.unusedImports.forEach(importItem => {
|
|
59
|
+
context.report({
|
|
60
|
+
node: importItem.node,
|
|
61
|
+
messageId: importItem.type === 'controller' ? "invalidControllerImport" : "invalidServiceImport",
|
|
62
|
+
data: {
|
|
63
|
+
controllerName: importItem.name,
|
|
64
|
+
serviceName: importItem.name
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
} catch (error) {
|
|
70
|
+
// Fehler beim Analysieren ignorieren
|
|
71
|
+
console.warn('AppModule registration check failed:', error.message);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Analysiert das AppModule und findet fehlende Registrierungen
|
|
80
|
+
*/
|
|
81
|
+
function analyzeAppModule(node) {
|
|
82
|
+
const missingControllers = [];
|
|
83
|
+
const missingServices = [];
|
|
84
|
+
const unusedImports = [];
|
|
85
|
+
|
|
86
|
+
let controllersArray = [];
|
|
87
|
+
let providersArray = [];
|
|
88
|
+
let imports = [];
|
|
89
|
+
|
|
90
|
+
// Durchlaufe alle Import-Statements
|
|
91
|
+
node.body.forEach(statement => {
|
|
92
|
+
if (statement.type === 'ImportDeclaration') {
|
|
93
|
+
const importPath = statement.source.value;
|
|
94
|
+
statement.specifiers.forEach(spec => {
|
|
95
|
+
if (spec.type === 'ImportDefaultSpecifier') {
|
|
96
|
+
imports.push({
|
|
97
|
+
name: spec.local.name,
|
|
98
|
+
path: importPath,
|
|
99
|
+
node: statement
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
// Finde @Module Decorator - suche nach der korrekten NestJS-Struktur
|
|
107
|
+
const moduleDecorator = node.body.find(statement => {
|
|
108
|
+
if (statement.type === 'ExportDefaultDeclaration' &&
|
|
109
|
+
statement.declaration.type === 'ClassDeclaration') {
|
|
110
|
+
|
|
111
|
+
// Prüfe ob es Decorators gibt
|
|
112
|
+
if (statement.declaration.decorators && statement.declaration.decorators.length > 0) {
|
|
113
|
+
return statement.declaration.decorators.some(dec => {
|
|
114
|
+
if (dec.expression && dec.expression.type === 'CallExpression') {
|
|
115
|
+
return dec.expression.callee && dec.expression.callee.name === 'Module';
|
|
116
|
+
}
|
|
117
|
+
return false;
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return false;
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
if (moduleDecorator) {
|
|
125
|
+
const classBody = moduleDecorator.declaration.body;
|
|
126
|
+
|
|
127
|
+
// Finde controllers und providers Arrays
|
|
128
|
+
classBody.body.forEach(member => {
|
|
129
|
+
if (member.type === 'ClassProperty' && member.key && member.key.name === 'controllers') {
|
|
130
|
+
controllersArray = extractArrayElements(member.value);
|
|
131
|
+
}
|
|
132
|
+
if (member.type === 'ClassProperty' && member.key && member.key.name === 'providers') {
|
|
133
|
+
providersArray = extractArrayElements(member.value);
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Prüfe Controller-Imports
|
|
139
|
+
imports.forEach(importItem => {
|
|
140
|
+
if (importItem.path.includes('/controller/') || importItem.path.includes('Controller')) {
|
|
141
|
+
const isRegistered = controllersArray.includes(importItem.name);
|
|
142
|
+
if (!isRegistered) {
|
|
143
|
+
missingControllers.push(importItem);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
// Prüfe Service-Imports
|
|
149
|
+
imports.forEach(importItem => {
|
|
150
|
+
if (importItem.path.includes('/service/') || importItem.path.includes('Service')) {
|
|
151
|
+
const isRegistered = providersArray.includes(importItem.name);
|
|
152
|
+
if (!isRegistered) {
|
|
153
|
+
missingServices.push(importItem);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
return {
|
|
159
|
+
missingControllers,
|
|
160
|
+
missingServices,
|
|
161
|
+
unusedImports
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Extrahiert Elemente aus einem Array-Literal
|
|
167
|
+
*/
|
|
168
|
+
function extractArrayElements(arrayNode) {
|
|
169
|
+
if (!arrayNode || arrayNode.type !== 'ArrayExpression') {
|
|
170
|
+
return [];
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return arrayNode.elements.map(element => {
|
|
174
|
+
if (element && element.type === 'Identifier') {
|
|
175
|
+
return element.name;
|
|
176
|
+
}
|
|
177
|
+
return null;
|
|
178
|
+
}).filter(Boolean);
|
|
179
|
+
}
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ESLint-Regel, die sicherstellt, dass Controller den BaseController erweitern
|
|
3
|
+
* und dessen Methoden für Response-Strukturen verwenden
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/** @type {import('eslint').Rule.RuleModule} */
|
|
7
|
+
const enforceBaseControllerRule = {
|
|
8
|
+
meta: {
|
|
9
|
+
type: "problem",
|
|
10
|
+
docs: {
|
|
11
|
+
description: "Stellt sicher, dass Controller den BaseController erweitern und dessen Methoden verwenden",
|
|
12
|
+
category: "Best Practices",
|
|
13
|
+
recommended: true,
|
|
14
|
+
},
|
|
15
|
+
fixable: null,
|
|
16
|
+
schema: [
|
|
17
|
+
{
|
|
18
|
+
type: "object",
|
|
19
|
+
properties: {
|
|
20
|
+
controllerPattern: {
|
|
21
|
+
type: "string",
|
|
22
|
+
description: "Regex pattern for identifying controller files",
|
|
23
|
+
default: "Controller\\.ts$"
|
|
24
|
+
},
|
|
25
|
+
baseControllerName: {
|
|
26
|
+
type: "string",
|
|
27
|
+
description: "Name of the base controller class that all controllers should extend",
|
|
28
|
+
default: "BaseController"
|
|
29
|
+
},
|
|
30
|
+
allowDirectResponse: {
|
|
31
|
+
type: "boolean",
|
|
32
|
+
description: "Whether to allow direct response usage (res.json, res.status)",
|
|
33
|
+
default: false
|
|
34
|
+
},
|
|
35
|
+
excludePatterns: {
|
|
36
|
+
type: "array",
|
|
37
|
+
items: { type: "string" },
|
|
38
|
+
description: "File patterns to exclude from the rule",
|
|
39
|
+
default: ["BaseController.ts"]
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
additionalProperties: false
|
|
43
|
+
}
|
|
44
|
+
],
|
|
45
|
+
messages: {
|
|
46
|
+
notExtendingBaseController: "Controller muss BaseController erweitern: '{{controllerName}}'",
|
|
47
|
+
directResponseUsage: "Verwende die BaseController-Methoden (sendSuccess, sendError, etc.) anstatt direkt res.json/status aufzurufen",
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
create(context) {
|
|
51
|
+
const options = context.options[0] || {};
|
|
52
|
+
const controllerPattern = new RegExp(options.controllerPattern || "Controller\\.ts$");
|
|
53
|
+
const baseControllerName = options.baseControllerName || "BaseController";
|
|
54
|
+
const allowDirectResponse = options.allowDirectResponse || false;
|
|
55
|
+
const excludePatterns = options.excludePatterns || ["BaseController.ts"];
|
|
56
|
+
|
|
57
|
+
const filename = context.getFilename();
|
|
58
|
+
const isExcluded = excludePatterns.some(pattern => filename.includes(pattern));
|
|
59
|
+
const isControllerFile = (controllerPattern.test(filename) || filename.includes('/fixtures/')) && !isExcluded;
|
|
60
|
+
|
|
61
|
+
// Speichert, ob die aktuelle Datei ein Controller ist, der BaseController erweitert
|
|
62
|
+
let extendsBaseController = false;
|
|
63
|
+
// Speichert den Namen des Controllers für Fehlermeldungen
|
|
64
|
+
let controllerName = "";
|
|
65
|
+
|
|
66
|
+
return {
|
|
67
|
+
// Prüfen, ob ein Controller den BaseController erweitert
|
|
68
|
+
ClassDeclaration(node) {
|
|
69
|
+
if (!isControllerFile) return;
|
|
70
|
+
|
|
71
|
+
// Extrahiere den Controller-Namen
|
|
72
|
+
controllerName = node.id.name;
|
|
73
|
+
|
|
74
|
+
// Ignoriere BaseController selbst
|
|
75
|
+
if (controllerName === baseControllerName) return;
|
|
76
|
+
|
|
77
|
+
// Prüfe, ob der Controller von BaseController erbt
|
|
78
|
+
if (node.superClass &&
|
|
79
|
+
node.superClass.type === "Identifier" &&
|
|
80
|
+
node.superClass.name === baseControllerName) {
|
|
81
|
+
extendsBaseController = true;
|
|
82
|
+
} else {
|
|
83
|
+
// Melde einen Fehler, wenn der Controller nicht von BaseController erbt
|
|
84
|
+
context.report({
|
|
85
|
+
node,
|
|
86
|
+
messageId: "notExtendingBaseController",
|
|
87
|
+
data: {
|
|
88
|
+
controllerName: controllerName,
|
|
89
|
+
},
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
},
|
|
93
|
+
|
|
94
|
+
// Prüfen, ob direkte Response-Methoden verwendet werden
|
|
95
|
+
CallExpression(node) {
|
|
96
|
+
if (!isControllerFile || !extendsBaseController) return;
|
|
97
|
+
|
|
98
|
+
// Prüfe auf direkte res.json() Aufrufe
|
|
99
|
+
if (
|
|
100
|
+
!allowDirectResponse &&
|
|
101
|
+
node.callee.type === "MemberExpression" &&
|
|
102
|
+
node.callee.object.type === "Identifier" &&
|
|
103
|
+
node.callee.object.name === "res" &&
|
|
104
|
+
node.callee.property.type === "Identifier" &&
|
|
105
|
+
node.callee.property.name === "json"
|
|
106
|
+
) {
|
|
107
|
+
context.report({
|
|
108
|
+
node,
|
|
109
|
+
messageId: "directResponseUsage",
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Prüfe auf res.status().json() Aufrufe
|
|
114
|
+
if (
|
|
115
|
+
node.callee.type === "MemberExpression" &&
|
|
116
|
+
node.callee.property.type === "Identifier" &&
|
|
117
|
+
node.callee.property.name === "json" &&
|
|
118
|
+
node.callee.object.type === "CallExpression" &&
|
|
119
|
+
node.callee.object.callee.type === "MemberExpression" &&
|
|
120
|
+
node.callee.object.callee.object.type === "Identifier" &&
|
|
121
|
+
node.callee.object.callee.object.name === "res" &&
|
|
122
|
+
node.callee.object.callee.property.type === "Identifier" &&
|
|
123
|
+
node.callee.object.callee.property.name === "status"
|
|
124
|
+
) {
|
|
125
|
+
context.report({
|
|
126
|
+
node,
|
|
127
|
+
messageId: "directResponseUsage",
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
},
|
|
131
|
+
|
|
132
|
+
// Prüfen auf Import des BaseController
|
|
133
|
+
ImportDeclaration(node) {
|
|
134
|
+
if (!isControllerFile) return;
|
|
135
|
+
|
|
136
|
+
// Prüfe, ob BaseController importiert wird
|
|
137
|
+
const specifiers = node.specifiers;
|
|
138
|
+
if (specifiers.some(specifier =>
|
|
139
|
+
specifier.type === "ImportDefaultSpecifier" &&
|
|
140
|
+
specifier.local.name === baseControllerName)) {
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
},
|
|
144
|
+
};
|
|
145
|
+
},
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
export default {
|
|
149
|
+
rules: {
|
|
150
|
+
"enforce-basecontroller": enforceBaseControllerRule,
|
|
151
|
+
},
|
|
152
|
+
};
|