@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,176 @@
|
|
|
1
|
+
export default {
|
|
2
|
+
rules: {
|
|
3
|
+
"enforce-dto-naming": {
|
|
4
|
+
meta: {
|
|
5
|
+
type: "problem",
|
|
6
|
+
docs: {
|
|
7
|
+
description: "Enforce correct DTO naming conventions and filename matching",
|
|
8
|
+
category: "Best Practices",
|
|
9
|
+
recommended: true,
|
|
10
|
+
},
|
|
11
|
+
fixable: null,
|
|
12
|
+
schema: [],
|
|
13
|
+
messages: {
|
|
14
|
+
invalidDtoSuffix: "Invalid DTO suffix pattern. Avoid double suffixes like 'DtoEntityDto', 'EntityDtoEntityDto', or 'EntityEntityDto'. Use only one suffix: 'EntityDto' for entity DTOs or 'Dto' for other DTOs.",
|
|
15
|
+
filenameMismatch: "Filename '{{filename}}' does not match class name '{{className}}'. Rename the file to '{{expectedFilename}}'.",
|
|
16
|
+
suggestCorrectName: "Consider renaming the class to '{{suggestedName}}' to match the filename.",
|
|
17
|
+
suggestCorrectFilename: "Consider renaming the file to '{{suggestedFilename}}' to match the class name.",
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
|
|
21
|
+
create(context) {
|
|
22
|
+
function hasInvalidDtoSuffix(className) {
|
|
23
|
+
// Verbotene Muster:
|
|
24
|
+
// - DtoEntityDto (doppeltes Dto)
|
|
25
|
+
// - EntityDtoEntityDto (doppeltes EntityDto)
|
|
26
|
+
// - EntityEntityDto (doppeltes Entity)
|
|
27
|
+
// - DtoDto (doppeltes Dto)
|
|
28
|
+
|
|
29
|
+
const invalidPatterns = [
|
|
30
|
+
/DtoEntityDto$/, // DtoEntityDto
|
|
31
|
+
/EntityDtoEntityDto$/, // EntityDtoEntityDto
|
|
32
|
+
/EntityEntityDto$/, // EntityEntityDto
|
|
33
|
+
/DtoDto$/, // DtoDto
|
|
34
|
+
/EntityEntity$/, // EntityEntity
|
|
35
|
+
/DtoEntityEntity$/, // DtoEntityEntity
|
|
36
|
+
/EntityDtoEntity$/, // EntityDtoEntity
|
|
37
|
+
];
|
|
38
|
+
|
|
39
|
+
return invalidPatterns.some(pattern => pattern.test(className));
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function getValidDtoSuffix(className) {
|
|
43
|
+
// Entferne alle bekannten Suffixe
|
|
44
|
+
let cleanName = className
|
|
45
|
+
.replace(/EntityDto$/, '')
|
|
46
|
+
.replace(/Dto$/, '')
|
|
47
|
+
.replace(/Entity$/, '');
|
|
48
|
+
|
|
49
|
+
// Füge den korrekten Suffix hinzu
|
|
50
|
+
if (className.endsWith('EntityDto')) {
|
|
51
|
+
return `${cleanName}EntityDto`;
|
|
52
|
+
} else if (className.endsWith('Dto')) {
|
|
53
|
+
return `${cleanName}Dto`;
|
|
54
|
+
} else if (className.endsWith('Entity')) {
|
|
55
|
+
return `${cleanName}Entity`;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Standardfall: Dto hinzufügen
|
|
59
|
+
return `${cleanName}Dto`;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function getExpectedFilename(className) {
|
|
63
|
+
return `${className}.ts`;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function getCurrentFilename(context) {
|
|
67
|
+
const filename = context.getFilename();
|
|
68
|
+
// Extrahiere den Dateinamen ohne Pfad und Erweiterung
|
|
69
|
+
const pathParts = filename.split('/');
|
|
70
|
+
const filenameWithExt = pathParts[pathParts.length - 1];
|
|
71
|
+
return filenameWithExt.replace(/\.ts$/, '');
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function isDtoFile(filename) {
|
|
75
|
+
// Prüfe, ob es sich um eine DTO-Datei handelt
|
|
76
|
+
return filename.includes('Dto') || filename.includes('Entity');
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function isTestFile(filename) {
|
|
80
|
+
return filename.includes('.test.') ||
|
|
81
|
+
filename.includes('.spec.') ||
|
|
82
|
+
filename.includes('__tests__');
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return {
|
|
86
|
+
ClassDeclaration(node) {
|
|
87
|
+
if (!node.id) return;
|
|
88
|
+
|
|
89
|
+
const className = node.id.name;
|
|
90
|
+
const currentFilename = getCurrentFilename(context);
|
|
91
|
+
|
|
92
|
+
// Überspringe Test-Dateien
|
|
93
|
+
if (isTestFile(currentFilename)) return;
|
|
94
|
+
|
|
95
|
+
// Prüfe nur DTO-Dateien
|
|
96
|
+
if (!isDtoFile(currentFilename)) return;
|
|
97
|
+
|
|
98
|
+
// Regel 1: Prüfe auf ungültige DTO-Suffixe
|
|
99
|
+
if (hasInvalidDtoSuffix(className)) {
|
|
100
|
+
const validName = getValidDtoSuffix(className);
|
|
101
|
+
|
|
102
|
+
context.report({
|
|
103
|
+
node: node.id,
|
|
104
|
+
messageId: "invalidDtoSuffix",
|
|
105
|
+
data: {
|
|
106
|
+
className,
|
|
107
|
+
suggestedName: validName,
|
|
108
|
+
},
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Regel 2: Prüfe auf Dateiname-Klassenname-Übereinstimmung
|
|
113
|
+
if (currentFilename !== className) {
|
|
114
|
+
const expectedFilename = getExpectedFilename(className);
|
|
115
|
+
|
|
116
|
+
context.report({
|
|
117
|
+
node: node.id,
|
|
118
|
+
messageId: "filenameMismatch",
|
|
119
|
+
data: {
|
|
120
|
+
filename: currentFilename,
|
|
121
|
+
className,
|
|
122
|
+
expectedFilename,
|
|
123
|
+
suggestedFilename: expectedFilename,
|
|
124
|
+
},
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
},
|
|
128
|
+
|
|
129
|
+
ExportDefaultDeclaration(node) {
|
|
130
|
+
if (node.declaration.type !== 'ClassDeclaration') return;
|
|
131
|
+
if (!node.declaration.id) return;
|
|
132
|
+
|
|
133
|
+
const className = node.declaration.id.name;
|
|
134
|
+
const currentFilename = getCurrentFilename(context);
|
|
135
|
+
|
|
136
|
+
// Überspringe Test-Dateien
|
|
137
|
+
if (isTestFile(currentFilename)) return;
|
|
138
|
+
|
|
139
|
+
// Prüfe nur DTO-Dateien
|
|
140
|
+
if (!isDtoFile(currentFilename)) return;
|
|
141
|
+
|
|
142
|
+
// Regel 1: Prüfe auf ungültige DTO-Suffixe
|
|
143
|
+
if (hasInvalidDtoSuffix(className)) {
|
|
144
|
+
const validName = getValidDtoSuffix(className);
|
|
145
|
+
|
|
146
|
+
context.report({
|
|
147
|
+
node: node.declaration.id,
|
|
148
|
+
messageId: "invalidDtoSuffix",
|
|
149
|
+
data: {
|
|
150
|
+
className,
|
|
151
|
+
suggestedName: validName,
|
|
152
|
+
},
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Regel 2: Prüfe auf Dateiname-Klassenname-Übereinstimmung
|
|
157
|
+
if (currentFilename !== className) {
|
|
158
|
+
const expectedFilename = getExpectedFilename(className);
|
|
159
|
+
|
|
160
|
+
context.report({
|
|
161
|
+
node: node.declaration.id,
|
|
162
|
+
messageId: "filenameMismatch",
|
|
163
|
+
data: {
|
|
164
|
+
filename: currentFilename,
|
|
165
|
+
className,
|
|
166
|
+
expectedFilename,
|
|
167
|
+
suggestedFilename: expectedFilename,
|
|
168
|
+
},
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
},
|
|
172
|
+
};
|
|
173
|
+
},
|
|
174
|
+
},
|
|
175
|
+
},
|
|
176
|
+
};
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
export default {
|
|
2
|
+
meta: {
|
|
3
|
+
type: "problem",
|
|
4
|
+
docs: {
|
|
5
|
+
description: "Enforce usage of DTOs instead of anonymous objects",
|
|
6
|
+
category: "Best Practices",
|
|
7
|
+
recommended: true,
|
|
8
|
+
},
|
|
9
|
+
fixable: null,
|
|
10
|
+
schema: [],
|
|
11
|
+
messages: {
|
|
12
|
+
anonymousObjectForbidden: "Anonymous objects are not allowed. Use a DTO class instead.",
|
|
13
|
+
anonymousObjectInReturn: "Returning anonymous objects is not allowed. Create a DTO class for the return type.",
|
|
14
|
+
anonymousObjectInAssignment: "Assigning anonymous objects is not allowed. Use a DTO class instead.",
|
|
15
|
+
anonymousObjectInFunctionCall: "Passing anonymous objects to functions is not allowed. Use a DTO class instead.",
|
|
16
|
+
},
|
|
17
|
+
},
|
|
18
|
+
|
|
19
|
+
create(context) {
|
|
20
|
+
function isTestFile() {
|
|
21
|
+
const filename = context.getFilename();
|
|
22
|
+
return filename.includes('.test.') || filename.includes('.spec.') || filename.includes('__tests__');
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function isExemptFunction(node) {
|
|
26
|
+
if (!node || !node.callee) return false;
|
|
27
|
+
|
|
28
|
+
// Check for method calls like logger.error
|
|
29
|
+
if (node.callee.type === "MemberExpression") {
|
|
30
|
+
const objectName = node.callee.object.name;
|
|
31
|
+
const propertyName = node.callee.property.name;
|
|
32
|
+
const fullName = `${objectName}.${propertyName}`;
|
|
33
|
+
|
|
34
|
+
// Allow logger methods
|
|
35
|
+
if (fullName.startsWith('logger.')) {
|
|
36
|
+
return true;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Allow console methods
|
|
40
|
+
if (fullName.startsWith('console.')) {
|
|
41
|
+
return true;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Allow DTO factory methods
|
|
45
|
+
if (propertyName === "create" && objectName && objectName.endsWith("Dto")) {
|
|
46
|
+
return true;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Check for direct function calls
|
|
51
|
+
if (node.callee.type === "Identifier") {
|
|
52
|
+
const exemptFunctions = [
|
|
53
|
+
"Object.assign", "JSON.stringify", "JSON.parse", "collectDefaultMetrics"
|
|
54
|
+
];
|
|
55
|
+
return exemptFunctions.includes(node.callee.name);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function isInsideExemptFunction(node) {
|
|
62
|
+
let parent = node.parent;
|
|
63
|
+
while (parent) {
|
|
64
|
+
if (parent.type === "CallExpression" && isExemptFunction(parent)) {
|
|
65
|
+
return true;
|
|
66
|
+
}
|
|
67
|
+
parent = parent.parent;
|
|
68
|
+
}
|
|
69
|
+
return false;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function checkObjectExpression(node) {
|
|
73
|
+
// Skip if it's a test file
|
|
74
|
+
if (isTestFile()) return;
|
|
75
|
+
|
|
76
|
+
// Skip if it's inside an exempt function
|
|
77
|
+
if (isInsideExemptFunction(node)) return;
|
|
78
|
+
|
|
79
|
+
// Skip if it's a simple object with only basic properties
|
|
80
|
+
if (node.properties && node.properties.length <= 2) {
|
|
81
|
+
const propertyNames = node.properties
|
|
82
|
+
.filter(prop => prop.type === "Property" && prop.key.type === "Identifier")
|
|
83
|
+
.map(prop => prop.key.name);
|
|
84
|
+
|
|
85
|
+
// Allow simple objects with basic properties
|
|
86
|
+
if (propertyNames.every(name => ["id", "isActive", "name", "type", "value"].includes(name))) {
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Report the anonymous object
|
|
92
|
+
let messageId = "anonymousObjectForbidden";
|
|
93
|
+
|
|
94
|
+
if (node.parent) {
|
|
95
|
+
if (node.parent.type === "ReturnStatement") {
|
|
96
|
+
messageId = "anonymousObjectInReturn";
|
|
97
|
+
} else if (node.parent.type === "VariableDeclarator") {
|
|
98
|
+
messageId = "anonymousObjectInAssignment";
|
|
99
|
+
} else if (node.parent.type === "CallExpression") {
|
|
100
|
+
messageId = "anonymousObjectInFunctionCall";
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
context.report({
|
|
105
|
+
node,
|
|
106
|
+
messageId,
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return {
|
|
111
|
+
ObjectExpression: checkObjectExpression,
|
|
112
|
+
};
|
|
113
|
+
},
|
|
114
|
+
};
|
|
@@ -0,0 +1,407 @@
|
|
|
1
|
+
export default {
|
|
2
|
+
rules: {
|
|
3
|
+
"enforce-dto-usage": {
|
|
4
|
+
meta: {
|
|
5
|
+
type: "problem",
|
|
6
|
+
docs: {
|
|
7
|
+
description: "Enforce usage of DTOs instead of anonymous objects",
|
|
8
|
+
category: "Best Practices",
|
|
9
|
+
recommended: true,
|
|
10
|
+
},
|
|
11
|
+
messages: {
|
|
12
|
+
anonymousObjectForbidden: "Anonymous objects are not allowed. Use DTO classes instead.",
|
|
13
|
+
anonymousObjectInReturn: "Returning anonymous objects is not allowed. Use a DTO class instead.",
|
|
14
|
+
anonymousObjectInAssignment: "Assigning anonymous objects is not allowed. Use a DTO class instead.",
|
|
15
|
+
anonymousObjectInFunctionCall: "Passing anonymous objects to functions is not allowed. Use a DTO class instead.",
|
|
16
|
+
},
|
|
17
|
+
},
|
|
18
|
+
|
|
19
|
+
create(context) {
|
|
20
|
+
function isTestFile() {
|
|
21
|
+
const filename = context.getFilename();
|
|
22
|
+
return filename.includes('.test.') || filename.includes('.spec.') || filename.includes('__tests__');
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function isDtoFile() {
|
|
26
|
+
const filename = context.getFilename();
|
|
27
|
+
return filename.includes('/dto/') || filename.includes('\\dto\\');
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function isExternalLibraryCall(node) {
|
|
31
|
+
if (!node || !node.callee) return false;
|
|
32
|
+
|
|
33
|
+
// Check for method calls like repository.findOne, repository.save, etc.
|
|
34
|
+
if (node.callee.type === "MemberExpression") {
|
|
35
|
+
const objectName = node.callee.object.name;
|
|
36
|
+
const propertyName = node.callee.property.name;
|
|
37
|
+
|
|
38
|
+
// TypeORM repository methods - check if object name suggests it's a repository
|
|
39
|
+
if (objectName && (
|
|
40
|
+
objectName.includes("Repository") ||
|
|
41
|
+
objectName === "repository" ||
|
|
42
|
+
objectName === "manager" ||
|
|
43
|
+
objectName === "transactionalEntityManager" ||
|
|
44
|
+
objectName === "entityManager" ||
|
|
45
|
+
objectName === "dataSource"
|
|
46
|
+
)) {
|
|
47
|
+
return true;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// TypeORM DataSource methods
|
|
51
|
+
if (objectName === "dataSource") {
|
|
52
|
+
return true;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Check if the call is to a method that's likely from an external library
|
|
56
|
+
// by checking if the object is imported from node_modules
|
|
57
|
+
const sourceCode = context.getSourceCode();
|
|
58
|
+
const nodeText = sourceCode.getText(node.callee.object);
|
|
59
|
+
|
|
60
|
+
// If the object is a property access chain that starts with a variable
|
|
61
|
+
// that's likely from an external library, consider it external
|
|
62
|
+
if (node.callee.object.type === "MemberExpression") {
|
|
63
|
+
const rootObject = node.callee.object.object;
|
|
64
|
+
if (rootObject && rootObject.type === "Identifier") {
|
|
65
|
+
const rootObjectName = rootObject.name;
|
|
66
|
+
// Common external library objects
|
|
67
|
+
const externalObjects = [
|
|
68
|
+
"dataSource", "repository", "manager", "entityManager",
|
|
69
|
+
"transactionalEntityManager", "queryRunner", "connection"
|
|
70
|
+
];
|
|
71
|
+
if (externalObjects.includes(rootObjectName)) {
|
|
72
|
+
return true;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Check for specific TypeORM method names
|
|
78
|
+
const typeOrmMethods = [
|
|
79
|
+
"findOne", "find", "save", "create", "update", "delete", "remove",
|
|
80
|
+
"findOneBy", "findBy", "findAndCount", "findAndCountBy",
|
|
81
|
+
"count", "countBy", "exists", "existsBy",
|
|
82
|
+
"query", "transaction", "getRepository", "createQueryBuilder",
|
|
83
|
+
"insert", "upsert", "softDelete", "restore", "recover",
|
|
84
|
+
"increment", "decrement", "clear"
|
|
85
|
+
];
|
|
86
|
+
|
|
87
|
+
if (typeOrmMethods.includes(propertyName)) {
|
|
88
|
+
return true;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return false;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function isExemptFunction(node) {
|
|
96
|
+
if (!node || !node.callee) return false;
|
|
97
|
+
|
|
98
|
+
// Check if it's an external library call first
|
|
99
|
+
if (isExternalLibraryCall(node)) {
|
|
100
|
+
return true;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Check for method calls like logger.error
|
|
104
|
+
if (node.callee.type === "MemberExpression") {
|
|
105
|
+
const objectName = node.callee.object.name;
|
|
106
|
+
const propertyName = node.callee.property.name;
|
|
107
|
+
const fullName = `${objectName}.${propertyName}`;
|
|
108
|
+
|
|
109
|
+
// Allow logger methods
|
|
110
|
+
if (fullName.startsWith('logger.')) {
|
|
111
|
+
return true;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Allow console methods
|
|
115
|
+
if (fullName.startsWith('console.')) {
|
|
116
|
+
return true;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Allow DTO factory methods (create, fromEntity, fromEntityArray, etc.)
|
|
120
|
+
if ((propertyName === "create" ||
|
|
121
|
+
propertyName === "fromEntity" ||
|
|
122
|
+
propertyName === "fromEntityArray" ||
|
|
123
|
+
propertyName === "fromRequestDto" ||
|
|
124
|
+
propertyName === "toEntity" ||
|
|
125
|
+
propertyName === "createInventorySlot") &&
|
|
126
|
+
objectName && objectName.endsWith("Dto")) {
|
|
127
|
+
return true;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Allow Service methods
|
|
131
|
+
if (objectName && (
|
|
132
|
+
objectName.endsWith("Service") ||
|
|
133
|
+
objectName.endsWith("Manager") ||
|
|
134
|
+
objectName.endsWith("Repository")
|
|
135
|
+
)) {
|
|
136
|
+
return true;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Allow this.service.method calls
|
|
140
|
+
if (objectName === "this" && propertyName && (
|
|
141
|
+
propertyName.endsWith("Service") ||
|
|
142
|
+
propertyName.endsWith("Manager") ||
|
|
143
|
+
propertyName.endsWith("Repository")
|
|
144
|
+
)) {
|
|
145
|
+
return true;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Check for direct function calls
|
|
150
|
+
if (node.callee.type === "Identifier") {
|
|
151
|
+
const exemptFunctions = [
|
|
152
|
+
"Object.assign", "JSON.stringify", "JSON.parse", "collectDefaultMetrics"
|
|
153
|
+
];
|
|
154
|
+
return exemptFunctions.includes(node.callee.name);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return false;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function isInsideExemptFunction(node) {
|
|
161
|
+
let parent = node.parent;
|
|
162
|
+
while (parent) {
|
|
163
|
+
if (parent.type === "CallExpression" && isExemptFunction(parent)) {
|
|
164
|
+
return true;
|
|
165
|
+
}
|
|
166
|
+
parent = parent.parent;
|
|
167
|
+
}
|
|
168
|
+
return false;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function isInsideDecorator(node) {
|
|
172
|
+
let parent = node.parent;
|
|
173
|
+
while (parent) {
|
|
174
|
+
// Check if we're inside a decorator call like @ApiResponse({...})
|
|
175
|
+
if (parent.type === "CallExpression" &&
|
|
176
|
+
parent.callee &&
|
|
177
|
+
parent.callee.type === "Identifier" &&
|
|
178
|
+
parent.callee.name &&
|
|
179
|
+
(parent.callee.name.startsWith("Api") ||
|
|
180
|
+
parent.callee.name.includes("Decorator") ||
|
|
181
|
+
parent.callee.name === "Column" ||
|
|
182
|
+
parent.callee.name === "Entity" ||
|
|
183
|
+
parent.callee.name === "PrimaryGeneratedColumn" ||
|
|
184
|
+
parent.callee.name === "CreateDateColumn" ||
|
|
185
|
+
parent.callee.name === "UpdateDateColumn" ||
|
|
186
|
+
parent.callee.name === "OneToMany" ||
|
|
187
|
+
parent.callee.name === "ManyToOne" ||
|
|
188
|
+
parent.callee.name === "ManyToMany" ||
|
|
189
|
+
parent.callee.name === "OneToOne" ||
|
|
190
|
+
parent.callee.name === "JoinColumn" ||
|
|
191
|
+
parent.callee.name === "Index" ||
|
|
192
|
+
parent.callee.name === "Unique")) {
|
|
193
|
+
return true;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Check if we're inside a decorator expression
|
|
197
|
+
if (parent.type === "Decorator") {
|
|
198
|
+
return true;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
parent = parent.parent;
|
|
202
|
+
}
|
|
203
|
+
return false;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function checkObjectExpression(node) {
|
|
207
|
+
// Skip if it's a test file
|
|
208
|
+
if (isTestFile()) return;
|
|
209
|
+
|
|
210
|
+
// Skip if it's inside an exempt function
|
|
211
|
+
if (isInsideExemptFunction(node)) return;
|
|
212
|
+
|
|
213
|
+
// Skip if it's inside a decorator
|
|
214
|
+
if (isInsideDecorator(node)) return;
|
|
215
|
+
|
|
216
|
+
// Skip if it's inside a DTO factory method
|
|
217
|
+
if (node.parent && node.parent.type === "CallExpression" &&
|
|
218
|
+
node.parent.callee && node.parent.callee.type === "MemberExpression" &&
|
|
219
|
+
node.parent.callee.property && node.parent.callee.property.name === "create" &&
|
|
220
|
+
node.parent.callee.object && node.parent.callee.object.name &&
|
|
221
|
+
(node.parent.callee.object.name.endsWith("Dto") || node.parent.callee.object.name.includes("Dto"))) {
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// Skip if it's a prom-client metric call
|
|
226
|
+
if (node.parent && node.parent.type === "CallExpression" &&
|
|
227
|
+
node.parent.callee && node.parent.callee.type === "MemberExpression" &&
|
|
228
|
+
node.parent.callee.property &&
|
|
229
|
+
["inc", "set", "observe", "histogram"].includes(node.parent.callee.property.name)) {
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// Skip if it's Object.assign with Error objects (common pattern for custom errors)
|
|
234
|
+
if (node.parent && node.parent.type === "CallExpression" &&
|
|
235
|
+
node.parent.callee && node.parent.callee.type === "MemberExpression" &&
|
|
236
|
+
node.parent.callee.object && node.parent.callee.object.name === "Object" &&
|
|
237
|
+
node.parent.callee.property && node.parent.callee.property.name === "assign" &&
|
|
238
|
+
node.parent.arguments && node.parent.arguments.length >= 2 &&
|
|
239
|
+
node.parent.arguments[0].type === "NewExpression" &&
|
|
240
|
+
node.parent.arguments[0].callee && node.parent.arguments[0].callee.name === "Error") {
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// Skip if in transformRequestToEntity method (common pattern for TypeORM)
|
|
245
|
+
const filename = context.getFilename();
|
|
246
|
+
if (filename.includes('/service/') && node.parent &&
|
|
247
|
+
node.parent.type === "ReturnStatement") {
|
|
248
|
+
let parent = node.parent;
|
|
249
|
+
while (parent) {
|
|
250
|
+
if (parent.type === "FunctionDeclaration" || parent.type === "MethodDefinition") {
|
|
251
|
+
const methodName = parent.key?.name || parent.id?.name;
|
|
252
|
+
if (methodName === "transformRequestToEntity") {
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
parent = parent.parent;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// Skip if in BaseController or ObservabilityController (generische Helper)
|
|
261
|
+
if (filename.includes('BaseController.ts') || filename.includes('ObservabilityController.ts')) {
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// Skip if it's a Prometheus Gauge/Counter/Histogram constructor
|
|
266
|
+
if (node.parent && node.parent.type === "NewExpression" &&
|
|
267
|
+
node.parent.callee && node.parent.callee.name &&
|
|
268
|
+
["Gauge", "Counter", "Histogram", "Summary"].includes(node.parent.callee.name)) {
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// Skip if it's a Zod schema definition (z.object)
|
|
273
|
+
if (node.parent && node.parent.type === "CallExpression" &&
|
|
274
|
+
node.parent.callee && node.parent.callee.type === "MemberExpression" &&
|
|
275
|
+
node.parent.callee.object && node.parent.callee.object.name === "z" &&
|
|
276
|
+
node.parent.callee.property && node.parent.callee.property.name === "object") {
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// Skip if it's assigned to a typed variable for logging or TypeORM options
|
|
281
|
+
if (node.parent && node.parent.type === "VariableDeclarator" &&
|
|
282
|
+
node.parent.id && node.parent.id.typeAnnotation) {
|
|
283
|
+
const typeAnnotation = context.getSourceCode().getText(node.parent.id.typeAnnotation);
|
|
284
|
+
if (typeAnnotation.includes("LogData") ||
|
|
285
|
+
typeAnnotation.includes("ChatLogData") ||
|
|
286
|
+
typeAnnotation.includes("AuthLogData") ||
|
|
287
|
+
typeAnnotation.includes("FindOptions") ||
|
|
288
|
+
typeAnnotation.includes("FindManyOptions") ||
|
|
289
|
+
typeAnnotation.includes("FindOneOptions")) {
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// Skip if it's a nested object inside a LogData or FindOptions variable
|
|
295
|
+
let parent = node.parent;
|
|
296
|
+
while (parent) {
|
|
297
|
+
if (parent.type === "VariableDeclarator" && parent.id && parent.id.typeAnnotation) {
|
|
298
|
+
const typeAnnotation = context.getSourceCode().getText(parent.id.typeAnnotation);
|
|
299
|
+
if (typeAnnotation.includes("LogData") ||
|
|
300
|
+
typeAnnotation.includes("AuthLogData") ||
|
|
301
|
+
typeAnnotation.includes("FindOptions") ||
|
|
302
|
+
typeAnnotation.includes("FindManyOptions") ||
|
|
303
|
+
typeAnnotation.includes("FindOneOptions")) {
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
parent = parent.parent;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// Skip if inside checkAiCommit method (test data arrays)
|
|
311
|
+
let checkParent = node.parent;
|
|
312
|
+
while (checkParent) {
|
|
313
|
+
if (checkParent.type === "MethodDefinition" || checkParent.type === "FunctionDeclaration") {
|
|
314
|
+
const methodName = checkParent.key?.name || checkParent.id?.name;
|
|
315
|
+
if (methodName === "checkAiCommit") {
|
|
316
|
+
return;
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
checkParent = checkParent.parent;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// Skip if in main.ts (Express/Server configuration)
|
|
323
|
+
if (filename.includes('main.ts') || filename.includes('server.ts')) {
|
|
324
|
+
return;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// Skip if in middleware files (Express middleware configuration)
|
|
328
|
+
if (filename.includes('/middleware/')) {
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// Skip if in Socket.ts (Socket.IO event handlers)
|
|
333
|
+
if (filename.includes('Socket.ts')) {
|
|
334
|
+
return;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// Skip if in module files (Modul-Konfiguration)
|
|
338
|
+
if (filename.includes('Module.ts')) {
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// Skip if in repository files (TypeORM where clauses and FindOptions)
|
|
343
|
+
if (filename.includes('/repository/')) {
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// Skip if in enum files (Enum configuration objects)
|
|
348
|
+
if (filename.includes('/enum/')) {
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// Skip if in migration/script files (Database migration objects)
|
|
353
|
+
if (filename.includes('/migration/') || filename.includes('/scripts/')) {
|
|
354
|
+
return;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// Skip if in service files (Business logic internal data structures)
|
|
358
|
+
if (filename.includes('/service/')) {
|
|
359
|
+
return;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// Skip if in guard/filter files (Auth logic)
|
|
363
|
+
if (filename.includes('/guards/') || filename.includes('/filters/')) {
|
|
364
|
+
return;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
// Skip if in utility files (Helper functions)
|
|
368
|
+
if (filename.includes('/utilities/')) {
|
|
369
|
+
return;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// Skip if in socket handler files (WebSocket events)
|
|
373
|
+
if (filename.includes('/socket/')) {
|
|
374
|
+
return;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// Skip if in config files (ORM configuration)
|
|
378
|
+
if (filename.includes('Ormconfig.ts') || filename.includes('ormconfig.ts')) {
|
|
379
|
+
return;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
// Report the anonymous object
|
|
383
|
+
let messageId = "anonymousObjectForbidden";
|
|
384
|
+
|
|
385
|
+
if (node.parent) {
|
|
386
|
+
if (node.parent.type === "ReturnStatement") {
|
|
387
|
+
messageId = "anonymousObjectInReturn";
|
|
388
|
+
} else if (node.parent.type === "VariableDeclarator" || node.parent.type === "AssignmentExpression") {
|
|
389
|
+
messageId = "anonymousObjectInAssignment";
|
|
390
|
+
} else if (node.parent.type === "CallExpression") {
|
|
391
|
+
messageId = "anonymousObjectInFunctionCall";
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
context.report({
|
|
396
|
+
node,
|
|
397
|
+
messageId,
|
|
398
|
+
});
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
return {
|
|
402
|
+
ObjectExpression: checkObjectExpression,
|
|
403
|
+
};
|
|
404
|
+
},
|
|
405
|
+
},
|
|
406
|
+
},
|
|
407
|
+
};
|