@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.
Files changed (171) hide show
  1. package/CHANGELOG.md +1093 -0
  2. package/configs/.gitkeep +1 -0
  3. package/configs/admin.js +203 -0
  4. package/configs/api-client.js +46 -0
  5. package/configs/backend.js +895 -0
  6. package/configs/domains.js +123 -0
  7. package/configs/frontend.js +30 -0
  8. package/configs/image-server.js +26 -0
  9. package/configs/ionos-proxy.js +372 -0
  10. package/configs/nestjs.js +156 -0
  11. package/configs/node.js +92 -0
  12. package/configs/react.js +111 -0
  13. package/configs/wiki.js +42 -0
  14. package/index.js +39 -0
  15. package/package.json +85 -0
  16. package/rules/.gitkeep +1 -0
  17. package/rules/__tests__/analyze-relation-usage.test.js.disabled +300 -0
  18. package/rules/__tests__/complexity.test.js.disabled +300 -0
  19. package/rules/__tests__/enforce-dto-factory-in-services.integration.test.js +226 -0
  20. package/rules/__tests__/enforce-dto-factory-in-services.test.js +177 -0
  21. package/rules/__tests__/enforce-entity-dto-create-no-id.integration.test.js +18 -0
  22. package/rules/__tests__/enforce-function-argument-count.test.js.disabled +300 -0
  23. package/rules/__tests__/enforce-repository-token-handling.test.js +58 -0
  24. package/rules/__tests__/english-only-code-strings.test.js.disabled +300 -0
  25. package/rules/__tests__/eslint-rules.integration.test.ts +350 -0
  26. package/rules/__tests__/integration-test-controller-response-dto.js +261 -0
  27. package/rules/__tests__/integration-test-dto-factory-in-services.js +260 -0
  28. package/rules/__tests__/integration-test-no-entity-type-casting.js +161 -0
  29. package/rules/__tests__/integration-test-typeorm-naming-conventions.js +501 -0
  30. package/rules/__tests__/test-config.js +33 -0
  31. package/rules/admin-controller-security.js +180 -0
  32. package/rules/analyze-relation-usage.js +687 -0
  33. package/rules/api-response-dto.js +174 -0
  34. package/rules/auth-guard-required.js +142 -0
  35. package/rules/backend-specific.js +36 -0
  36. package/rules/best-practices.js +421 -0
  37. package/rules/complexity.js +20 -0
  38. package/rules/controller-architecture.js +340 -0
  39. package/rules/controller-naming-conventions.js +190 -0
  40. package/rules/controller-readonly-restriction.js +148 -0
  41. package/rules/controller-swagger-complete.js +312 -0
  42. package/rules/controller-swagger-docs.js +119 -0
  43. package/rules/controller-swagger-english.js +320 -0
  44. package/rules/coordinate-naming.js +132 -0
  45. package/rules/custom-mui-button.js +135 -0
  46. package/rules/dead-code-detection-backend.js +50 -0
  47. package/rules/dead-code-detection-frontend.js +48 -0
  48. package/rules/dead-code-detection.js +71 -0
  49. package/rules/debug-controller-response-dto.js +79 -0
  50. package/rules/deprecate.js +8 -0
  51. package/rules/dto-annotation-property-consistency.js +111 -0
  52. package/rules/dto-entity-mapping-completeness.js +688 -0
  53. package/rules/dto-entity-swagger-separation.js +265 -0
  54. package/rules/dto-entity-type-consistency.js +352 -0
  55. package/rules/dto-entity-type-matching.js +519 -0
  56. package/rules/dto-naming-convention.js +98 -0
  57. package/rules/dto-visibility-modifiers.js +159 -0
  58. package/rules/enforce-api-versioning.js +122 -0
  59. package/rules/enforce-app-module-registration.js +179 -0
  60. package/rules/enforce-basecontroller.js +152 -0
  61. package/rules/enforce-body-request-dto.js +141 -0
  62. package/rules/enforce-controller-response-dto.js +349 -0
  63. package/rules/enforce-custom-error-classes.js +242 -0
  64. package/rules/enforce-database-transaction-safety.js +179 -0
  65. package/rules/enforce-dto-constructor.js +95 -0
  66. package/rules/enforce-dto-create-parameter-types.js +170 -0
  67. package/rules/enforce-dto-create-pattern.js +274 -0
  68. package/rules/enforce-dto-entity-creation.js +164 -0
  69. package/rules/enforce-dto-factory-in-services.js +188 -0
  70. package/rules/enforce-dto-from-entity-method.js +47 -0
  71. package/rules/enforce-dto-from-entity.js +314 -0
  72. package/rules/enforce-dto-naming-conventions.js +212 -0
  73. package/rules/enforce-dto-naming.js +176 -0
  74. package/rules/enforce-dto-usage-simple.js +114 -0
  75. package/rules/enforce-dto-usage.js +407 -0
  76. package/rules/enforce-eager-translation-loading.js +178 -0
  77. package/rules/enforce-entity-creation-pattern.js +137 -0
  78. package/rules/enforce-entity-dto-convert-method.js +157 -0
  79. package/rules/enforce-entity-dto-create-no-id.js +117 -0
  80. package/rules/enforce-entity-dto-extends-base.js +141 -0
  81. package/rules/enforce-entity-dto-from-request-dto-structure.js +113 -0
  82. package/rules/enforce-entity-dto-fromentity-complex.js +69 -0
  83. package/rules/enforce-entity-dto-fromentity-simple.js +69 -0
  84. package/rules/enforce-entity-dto-fromrequestdto-structure.js +262 -0
  85. package/rules/enforce-entity-dto-methods-restriction.js +159 -0
  86. package/rules/enforce-entity-dto-no-request-dto.js +102 -0
  87. package/rules/enforce-entity-dto-optional-auto-fields.js +101 -0
  88. package/rules/enforce-entity-dto-required-methods.js +248 -0
  89. package/rules/enforce-entity-factory-pattern.js +180 -0
  90. package/rules/enforce-entity-instantiation-in-toentity.js +125 -0
  91. package/rules/enforce-enum-for-playable-entities.js +95 -0
  92. package/rules/enforce-error-handling.js +257 -0
  93. package/rules/enforce-explicit-dto-types.js +118 -0
  94. package/rules/enforce-from-request-dto-usage.js +62 -0
  95. package/rules/enforce-generic-entity-dto.js +71 -0
  96. package/rules/enforce-inject-decorator.js +133 -0
  97. package/rules/enforce-lazy-type-loading.js +170 -0
  98. package/rules/enforce-module-existence.js +157 -0
  99. package/rules/enforce-nonentity-dto-create.js +107 -0
  100. package/rules/enforce-playable-entity-naming.js +108 -0
  101. package/rules/enforce-repository-token-handling.js +92 -0
  102. package/rules/enforce-request-dto-no-entity-dto.js +201 -0
  103. package/rules/enforce-request-dto-required-fields.js +217 -0
  104. package/rules/enforce-result-pattern.js +45 -0
  105. package/rules/enforce-service-relation-loading.js +116 -0
  106. package/rules/enforce-test-coverage.js +96 -0
  107. package/rules/enforce-toentity-conditional-assignment.js +132 -0
  108. package/rules/enforce-translations-required.js +203 -0
  109. package/rules/enforce-typeorm-naming-conventions.js +366 -0
  110. package/rules/enforce-vite-health-metrics.js +240 -0
  111. package/rules/entity-required-properties.js +321 -0
  112. package/rules/entity-to-dto-test.js +73 -0
  113. package/rules/enum-database-validation.js +149 -0
  114. package/rules/errors.js +190 -0
  115. package/rules/es6.js +204 -0
  116. package/rules/eslint-plugin-no-comments.js +44 -0
  117. package/rules/filename-class-name-match.js +62 -0
  118. package/rules/forbid-fromentity-outside-entity-folder.js +237 -0
  119. package/rules/function-params-newline.js +111 -0
  120. package/rules/imports.js +264 -0
  121. package/rules/jest.js +13 -0
  122. package/rules/jsx.js +16 -0
  123. package/rules/max-classes-per-file.js +49 -0
  124. package/rules/multiline-formatting.js +146 -0
  125. package/rules/no-blank-lines-between-decorators-and-properties.js +95 -0
  126. package/rules/no-comments.js +62 -0
  127. package/rules/no-dto-constructors.js +126 -0
  128. package/rules/no-dto-default-values.js +220 -0
  129. package/rules/no-dto-duplicates.js +127 -0
  130. package/rules/no-dto-in-entity.js +99 -0
  131. package/rules/no-dynamic-import-in-types.js +71 -0
  132. package/rules/no-dynamic-imports-in-controllers.js +95 -0
  133. package/rules/no-entity-imports-in-controllers.js +101 -0
  134. package/rules/no-entity-in-swagger-docs.js +139 -0
  135. package/rules/no-entity-type-casting.js +104 -0
  136. package/rules/no-fetch.js +77 -0
  137. package/rules/no-import-meta-env.js +151 -0
  138. package/rules/no-inline-styles.js +5 -0
  139. package/rules/no-magic-values.js +85 -0
  140. package/rules/no-partial-type.js +168 -0
  141. package/rules/no-relative-imports.js +31 -0
  142. package/rules/no-tsyringe.js +181 -0
  143. package/rules/no-type-assertion.js +175 -0
  144. package/rules/no-undefined-entity-properties.js +121 -0
  145. package/rules/node.js +44 -0
  146. package/rules/perfectionist.js +50 -0
  147. package/rules/performance-minimal.js +155 -0
  148. package/rules/performance.js +44 -0
  149. package/rules/pino-logger-format.js +200 -0
  150. package/rules/prefer-dto-classes.js +112 -0
  151. package/rules/prefer-dto-create-method.js +225 -0
  152. package/rules/promises.js +17 -0
  153. package/rules/react-hooks.js +15 -0
  154. package/rules/react.js +28 -0
  155. package/rules/regexp.js +70 -0
  156. package/rules/require-dto-response.js +81 -0
  157. package/rules/require-valid-relations.js +388 -0
  158. package/rules/result-pattern.js +162 -0
  159. package/rules/security.js +37 -0
  160. package/rules/service-architecture.js +148 -0
  161. package/rules/sonarjs.js +26 -0
  162. package/rules/strict.js +7 -0
  163. package/rules/style.js +611 -0
  164. package/rules/stylistic.js +93 -0
  165. package/rules/typeorm-column-type-validation.js +224 -0
  166. package/rules/typescript-advanced.js +113 -0
  167. package/rules/typescript-core.js +111 -0
  168. package/rules/typescript.js +146 -0
  169. package/rules/unicorn.js +168 -0
  170. package/rules/variables.js +51 -0
  171. package/rules/websocket-architecture.js +115 -0
@@ -0,0 +1,687 @@
1
+ /**
2
+ * @fileoverview ESLint rule to analyze relation usage through static code analysis
3
+ * @description Traces data flow from repository calls to DTO usage to identify missing relations
4
+ */
5
+
6
+ import fs from 'fs';
7
+ import path from 'path';
8
+
9
+ const analyzeRelationUsageRule = {
10
+ meta: {
11
+ type: "suggestion",
12
+ docs: {
13
+ description: "Analyze data flow to identify potentially missing TypeORM relations",
14
+ category: "TypeORM",
15
+ recommended: true,
16
+ },
17
+ fixable: null,
18
+ schema: [
19
+ {
20
+ type: "object",
21
+ properties: {
22
+ traceMethods: {
23
+ type: "array",
24
+ items: { type: "string" },
25
+ description: "Method patterns to trace (e.g., '*ForDto', '*ToApi')",
26
+ },
27
+ dtoPatterns: {
28
+ type: "array",
29
+ items: { type: "string" },
30
+ description: "DTO class name patterns to analyze",
31
+ },
32
+ entityDirectory: {
33
+ type: "string",
34
+ description: "Path to entity directory for external entity analysis",
35
+ },
36
+ },
37
+ additionalProperties: false,
38
+ },
39
+ ],
40
+ messages: {
41
+ potentialMissingRelation: "Method '{{methodName}}' may need relation '{{relationName}}' - used in DTO '{{dtoName}}' but not loaded in repository call",
42
+ suggestRelationLoad: "Consider adding relations: [{{suggestedRelations}}] to repository call in '{{methodName}}'",
43
+ invalidRelation: "Relation '{{relationName}}' does not exist in Entity '{{entityName}}'. Available relations: [{{availableRelations}}]",
44
+ entityNotFound: "Entity '{{entityName}}' not found for relation validation",
45
+ },
46
+ },
47
+
48
+ create(context) {
49
+ const options = context.options[0] || {};
50
+ const traceMethods = options.traceMethods || ["*ForDto", "*ToApi", "getAll", "getBy*"];
51
+ const dtoPatterns = options.dtoPatterns || ["*Dto"];
52
+ const entityDirectory = options.entityDirectory || path.join(context.getCwd(), 'src/entity');
53
+
54
+ // Data structures for analysis
55
+ const serviceMethodCalls = new Map(); // method -> repository calls
56
+ const dtoPropertyAccess = new Map(); // DTO class -> accessed properties
57
+ const entityRelations = new Map(); // Entity -> relations (static cache)
58
+ const dataFlow = new Map(); // method -> DTOs used
59
+ const repositoryMethods = new Map(); // methodName -> entityName
60
+ let currentMethod = null; // Track current method being analyzed
61
+ const visitedNodes = new Set();
62
+
63
+ /**
64
+ * Load entity relations from external files
65
+ */
66
+ function loadEntityRelations(entityName) {
67
+ if (entityRelations.has(entityName)) {
68
+ return entityRelations.get(entityName);
69
+ }
70
+
71
+ const relations = [];
72
+
73
+ /**
74
+ * Recursively search for entity file in all subdirectories
75
+ */
76
+ function findEntityFile(directory) {
77
+ try {
78
+ if (!fs.existsSync(directory)) {
79
+ return null;
80
+ }
81
+
82
+ const items = fs.readdirSync(directory);
83
+
84
+ // First, check if the entity file exists directly in this directory
85
+ const directFile = path.join(directory, `${entityName}.ts`);
86
+ if (fs.existsSync(directFile)) {
87
+ return directFile;
88
+ }
89
+
90
+ // Then, recursively search subdirectories
91
+ for (const item of items) {
92
+ const itemPath = path.join(directory, item);
93
+ const stat = fs.statSync(itemPath);
94
+
95
+ if (stat.isDirectory()) {
96
+ const found = findEntityFile(itemPath);
97
+ if (found) {
98
+ return found;
99
+ }
100
+ }
101
+ }
102
+
103
+ return null;
104
+ } catch (error) {
105
+ return null;
106
+ }
107
+ }
108
+
109
+ const entityPath = findEntityFile(entityDirectory);
110
+
111
+ if (entityPath) {
112
+ try {
113
+ const content = fs.readFileSync(entityPath, 'utf8');
114
+
115
+ // Parse relations using line-by-line approach for reliability
116
+ // TypeORM decorators are often on the line before the property declaration
117
+ const lines = content.split('\n');
118
+ for (let i = 0; i < lines.length - 1; i++) {
119
+ const line = lines[i].trim();
120
+ const nextLine = lines[i + 1].trim();
121
+
122
+ // Check if current line has a relation decorator
123
+ if (line.match(/@(?:OneToMany|ManyToOne|OneToOne|ManyToMany)/)) {
124
+ // Check if next line has a property declaration
125
+ const propertyMatch = nextLine.match(/(\w+)\s*:/);
126
+ if (propertyMatch) {
127
+ const relationName = propertyMatch[1];
128
+ if (!relations.includes(relationName)) {
129
+ relations.push(relationName);
130
+ }
131
+ }
132
+ }
133
+ }
134
+ } catch (error) {
135
+ // Error reading file, continue
136
+ }
137
+ } else {
138
+ // Debug: Log when entity file is not found
139
+ console.log(`[analyze-relation-usage] Entity file not found for: ${entityName} in ${entityDirectory}`);
140
+ }
141
+
142
+ entityRelations.set(entityName, relations);
143
+ return relations;
144
+ }
145
+
146
+ /**
147
+ * Check if method name matches trace patterns
148
+ */
149
+ function shouldTraceMethod(methodName) {
150
+ return traceMethods.some(pattern => {
151
+ const regex = new RegExp(pattern.replace(/\*/g, ".*"));
152
+ return regex.test(methodName);
153
+ });
154
+ }
155
+
156
+ /**
157
+ * Check if class name matches DTO patterns
158
+ */
159
+ function isDtoClass(className) {
160
+ return dtoPatterns.some(pattern => {
161
+ const regex = new RegExp(pattern.replace(/\*/g, ".*"));
162
+ return regex.test(className);
163
+ });
164
+ }
165
+
166
+ /**
167
+ * Extract entity name from repository call - robust version
168
+ */
169
+ function getEntityFromRepositoryCall(node) {
170
+ // Case 1: Direct getRepository call with entity argument
171
+ // this.dataSource.getRepository(EntityClass)
172
+ if (
173
+ node.callee?.property?.name === "getRepository" &&
174
+ node.arguments?.[0]?.name
175
+ ) {
176
+ return node.arguments[0].name;
177
+ }
178
+
179
+ // Case 2: Method call on repository object
180
+ // raceRepository.findOne() or this.getRepository().findOne()
181
+ if (node.callee?.object?.type === "CallExpression") {
182
+ const repositoryCall = node.callee.object;
183
+
184
+ // Direct getRepository call with entity argument
185
+ if (repositoryCall.callee?.property?.name === "getRepository" &&
186
+ repositoryCall.arguments?.[0]?.name) {
187
+ return repositoryCall.arguments[0].name;
188
+ }
189
+
190
+ // Method call like this.getRepository() - trace back to method definition
191
+ if (repositoryCall.callee?.property?.name === "getRepository") {
192
+ return findEntityFromMethodDefinition(repositoryCall);
193
+ }
194
+ }
195
+
196
+ // Case 3: Repository variable name pattern
197
+ // raceRepository.findOne() -> PlayableRaceEntity
198
+ if (node.callee?.object?.name) {
199
+ const objectName = node.callee.object.name;
200
+ if (objectName.includes("Repository")) {
201
+ return findEntityFromRepositoryVariable(node, objectName);
202
+ }
203
+ }
204
+
205
+ // Case 4: Variable declaration context
206
+ // const raceRepository = this.dataSource.getRepository(PlayableRaceEntity);
207
+ return findEntityFromVariableDeclaration(node);
208
+ }
209
+
210
+ /**
211
+ * Find entity from method definition (e.g., getRepository method)
212
+ */
213
+ function findEntityFromMethodDefinition(repositoryCall) {
214
+ // Walk up the AST to find the method definition
215
+ let currentNode = repositoryCall;
216
+ while (currentNode && currentNode.parent) {
217
+ currentNode = currentNode.parent;
218
+
219
+ // Found a method definition
220
+ if (currentNode.type === "MethodDefinition") {
221
+ const methodName = currentNode.key?.name;
222
+
223
+ // Look for getRepository method
224
+ if (methodName === "getRepository") {
225
+ const methodBody = currentNode.value?.body;
226
+ if (methodBody?.body) {
227
+ // Look for return statement with getRepository call
228
+ for (const statement of methodBody.body) {
229
+ if (statement.type === "ReturnStatement" &&
230
+ statement.argument?.type === "CallExpression" &&
231
+ statement.argument.callee?.property?.name === "getRepository" &&
232
+ statement.argument.arguments?.[0]?.name) {
233
+ return statement.argument.arguments[0].name;
234
+ }
235
+ }
236
+ }
237
+ }
238
+ }
239
+
240
+ // Also check if we're in a service class and look for patterns
241
+ if (currentNode.type === "ClassDeclaration") {
242
+ const className = currentNode.id?.name;
243
+ if (className && className.includes("Service")) {
244
+ // Look for common patterns in service classes
245
+ const serviceBody = currentNode.body?.body;
246
+ if (serviceBody) {
247
+ for (const member of serviceBody) {
248
+ if (member.type === "MethodDefinition" &&
249
+ member.key?.name === "getRepository") {
250
+ const methodBody = member.value?.body;
251
+ if (methodBody?.body) {
252
+ for (const statement of methodBody.body) {
253
+ if (statement.type === "ReturnStatement" &&
254
+ statement.argument?.type === "CallExpression" &&
255
+ statement.argument.callee?.property?.name === "getRepository" &&
256
+ statement.argument.arguments?.[0]?.name) {
257
+ return statement.argument.arguments[0].name;
258
+ }
259
+ }
260
+ }
261
+ }
262
+ }
263
+ }
264
+ }
265
+ }
266
+ }
267
+
268
+ return null;
269
+ }
270
+
271
+ /**
272
+ * Find entity from repository variable name
273
+ */
274
+ function findEntityFromRepositoryVariable(node, objectName) {
275
+ // Walk up to find where this variable was declared
276
+ let currentNode = node;
277
+ while (currentNode && currentNode.parent) {
278
+ currentNode = currentNode.parent;
279
+
280
+ // Check variable declarations
281
+ if (currentNode.type === "VariableDeclarator" &&
282
+ currentNode.id?.name === objectName) {
283
+ // Check if it's assigned from getRepository
284
+ if (currentNode.init?.type === "CallExpression" &&
285
+ currentNode.init.callee?.property?.name === "getRepository" &&
286
+ currentNode.init.arguments?.[0]?.name) {
287
+ return currentNode.init.arguments[0].name;
288
+ }
289
+ }
290
+
291
+ // Check method calls that might assign the repository
292
+ if (currentNode.type === "CallExpression" &&
293
+ currentNode.callee?.property?.name === "getRepository" &&
294
+ currentNode.arguments?.[0]?.name) {
295
+ // Check if this call is assigned to our repository variable
296
+ const parent = currentNode.parent;
297
+ if (parent?.type === "VariableDeclarator" &&
298
+ parent.id?.name === objectName) {
299
+ return currentNode.arguments[0].name;
300
+ }
301
+ }
302
+ }
303
+
304
+ return null;
305
+ }
306
+
307
+ /**
308
+ * Find entity from variable declaration context
309
+ */
310
+ function findEntityFromVariableDeclaration(node) {
311
+ let currentNode = node;
312
+ while (currentNode && currentNode.parent) {
313
+ currentNode = currentNode.parent;
314
+
315
+ if (currentNode.type === "VariableDeclarator" && currentNode.id?.name) {
316
+ const varName = currentNode.id.name;
317
+ if (varName.includes("Repository")) {
318
+ // Check if it's assigned from getRepository
319
+ if (currentNode.init?.type === "CallExpression" &&
320
+ currentNode.init.callee?.property?.name === "getRepository" &&
321
+ currentNode.init.arguments?.[0]?.name) {
322
+ return currentNode.init.arguments[0].name;
323
+ }
324
+ }
325
+ }
326
+ }
327
+
328
+ return null;
329
+ }
330
+
331
+ /**
332
+ * Extract relations from repository find options
333
+ */
334
+ function getRelationsFromFindOptions(node) {
335
+ const optionsArg = node.arguments?.[0];
336
+ if (!optionsArg || optionsArg.type !== "ObjectExpression") {
337
+ return [];
338
+ }
339
+
340
+ const relationsProperty = optionsArg.properties?.find(
341
+ prop => prop.key?.name === "relations"
342
+ );
343
+
344
+ if (!relationsProperty || relationsProperty.value?.type !== "ArrayExpression") {
345
+ return [];
346
+ }
347
+
348
+ const relations = relationsProperty.value.elements
349
+ ?.filter(el => el?.type === "Literal" && typeof el.value === "string")
350
+ ?.map(el => el.value) || [];
351
+
352
+ // For nested relations like "raceAbilities.ability", we need to validate
353
+ // that the base relation exists and the nested relation exists in the related entity
354
+ return relations;
355
+ }
356
+
357
+ /**
358
+ * Validate nested relations (e.g., "raceAbilities.ability")
359
+ */
360
+ function validateNestedRelation(entityName, relationPath) {
361
+ const parts = relationPath.split('.');
362
+ if (parts.length === 1) {
363
+ // Simple relation - already validated by main logic
364
+ return true;
365
+ }
366
+
367
+ const [baseRelation, ...nestedParts] = parts;
368
+
369
+ // First, check if base relation exists in the main entity
370
+ const entityRelations = loadEntityRelations(entityName);
371
+ if (!entityRelations.includes(baseRelation)) {
372
+ return false;
373
+ }
374
+
375
+ // Find the related entity for the base relation
376
+ const relatedEntityName = findRelatedEntityName(entityName, baseRelation);
377
+ if (!relatedEntityName) {
378
+ return false;
379
+ }
380
+
381
+ // Recursively validate nested relations
382
+ const nestedRelation = nestedParts.join('.');
383
+ return validateNestedRelation(relatedEntityName, nestedRelation);
384
+ }
385
+
386
+ /**
387
+ * Find the related entity name for a given relation
388
+ */
389
+ function findRelatedEntityName(entityName, relationName) {
390
+ // Try to load the entity file and find the relation type
391
+ const entityPath = findEntityFile(entityName);
392
+ if (!entityPath) {
393
+ return null;
394
+ }
395
+
396
+ try {
397
+ const content = fs.readFileSync(entityPath, 'utf8');
398
+ const lines = content.split('\n');
399
+
400
+ for (let i = 0; i < lines.length - 1; i++) {
401
+ const line = lines[i].trim();
402
+ const nextLine = lines[i + 1].trim();
403
+
404
+ // Check if current line has a relation decorator and next line has our relation
405
+ if (line.match(/@(?:OneToMany|ManyToOne|OneToOne|ManyToMany)/) &&
406
+ nextLine.includes(relationName)) {
407
+
408
+ // Extract the entity type from the decorator
409
+ const entityMatch = line.match(/\(\(\) => (\w+)/);
410
+ if (entityMatch) {
411
+ return entityMatch[1];
412
+ }
413
+ }
414
+ }
415
+ } catch (error) {
416
+ // Error reading file, continue
417
+ }
418
+
419
+ return null;
420
+ }
421
+
422
+ /**
423
+ * Find entity file path
424
+ */
425
+ function findEntityFile(entityName) {
426
+ const entityDirectory = path.join(context.getCwd(), 'src/entity');
427
+
428
+ function searchDirectory(directory) {
429
+ try {
430
+ if (!fs.existsSync(directory)) {
431
+ return null;
432
+ }
433
+
434
+ const items = fs.readdirSync(directory);
435
+
436
+ // First, check if the entity file exists directly in this directory
437
+ const directFile = path.join(directory, `${entityName}.ts`);
438
+ if (fs.existsSync(directFile)) {
439
+ return directFile;
440
+ }
441
+
442
+ // Then, recursively search subdirectories
443
+ for (const item of items) {
444
+ const itemPath = path.join(directory, item);
445
+ const stat = fs.statSync(itemPath);
446
+
447
+ if (stat.isDirectory()) {
448
+ const found = searchDirectory(itemPath);
449
+ if (found) {
450
+ return found;
451
+ }
452
+ }
453
+ }
454
+
455
+ return null;
456
+ } catch (error) {
457
+ return null;
458
+ }
459
+ }
460
+
461
+ return searchDirectory(entityDirectory);
462
+ }
463
+
464
+ /**
465
+ * Analyze property access in DTO constructor/fromEntity method
466
+ */
467
+ function analyzeDtoPropertyAccess(node, dtoClassName) {
468
+ if (!isDtoClass(dtoClassName)) return;
469
+
470
+ const accessedProperties = new Set();
471
+
472
+ // Walk through method body to find property accesses
473
+ function walkNode(n) {
474
+ if (!n || typeof n !== "object" || visitedNodes.has(n)) {
475
+ return;
476
+ }
477
+
478
+ visitedNodes.add(n);
479
+ if (n.type === "MemberExpression" &&
480
+ (n.object?.name === "entity" || n.object?.name === "item")) {
481
+ if (n.property?.name) {
482
+ accessedProperties.add(n.property.name);
483
+ }
484
+ }
485
+
486
+ // Recursively walk child nodes
487
+ Object.values(n).forEach(child => {
488
+ if (child && typeof child === "object" && child.type) {
489
+ walkNode(child);
490
+ } else if (Array.isArray(child)) {
491
+ child.forEach(item => item && typeof item === "object" && item.type && walkNode(item));
492
+ }
493
+ });
494
+ }
495
+
496
+ walkNode(node);
497
+ dtoPropertyAccess.set(dtoClassName, accessedProperties);
498
+ }
499
+
500
+ /**
501
+ * Track method calls to DTOs
502
+ */
503
+ function trackDtoUsage(node, methodName) {
504
+ // Look for DTO.fromEntity() calls or new DTO() calls
505
+ if (node.type === "CallExpression") {
506
+ if (node.callee?.property?.name === "fromEntity" && node.callee?.object?.name) {
507
+ const dtoName = node.callee.object.name;
508
+ if (isDtoClass(dtoName)) {
509
+ if (!dataFlow.has(methodName)) {
510
+ dataFlow.set(methodName, new Set());
511
+ }
512
+ dataFlow.get(methodName).add(dtoName);
513
+ }
514
+ }
515
+
516
+ if (node.callee?.name && isDtoClass(node.callee.name)) {
517
+ if (!dataFlow.has(methodName)) {
518
+ dataFlow.set(methodName, new Set());
519
+ }
520
+ dataFlow.get(methodName).add(node.callee.name);
521
+ }
522
+ }
523
+ }
524
+
525
+ return {
526
+ // Collect entity relations from @Entity classes in current file
527
+ ClassDeclaration(node) {
528
+ if (node.decorators?.some(dec => dec.expression?.callee?.name === "Entity")) {
529
+ const className = node.id?.name;
530
+ const relations = [];
531
+
532
+ node.body?.body?.forEach(member => {
533
+ if (member.type === "PropertyDefinition" && member.decorators) {
534
+ const hasRelationDecorator = member.decorators.some(decorator => {
535
+ const decoratorName = decorator.expression?.callee?.name || decorator.expression?.name;
536
+ return ["OneToMany", "ManyToOne", "OneToOne", "ManyToMany"].includes(decoratorName);
537
+ });
538
+
539
+ if (hasRelationDecorator && member.key?.name) {
540
+ relations.push(member.key.name);
541
+ }
542
+ }
543
+ });
544
+
545
+ if (className && relations.length > 0) {
546
+ entityRelations.set(className, relations);
547
+ }
548
+ }
549
+ },
550
+
551
+ // Analyze DTO classes
552
+ MethodDefinition(node) {
553
+ const className = node.parent?.parent?.id?.name;
554
+ if (className && isDtoClass(className)) {
555
+ if (node.key?.name === "constructor" || node.key?.name === "fromEntity") {
556
+ analyzeDtoPropertyAccess(node.value?.body, className);
557
+ }
558
+ }
559
+
560
+ // Track service methods
561
+ const methodName = node.key?.name;
562
+ if (methodName && shouldTraceMethod(methodName)) {
563
+ currentMethod = methodName;
564
+ }
565
+ },
566
+
567
+ "MethodDefinition:exit"() {
568
+ currentMethod = null;
569
+ },
570
+
571
+ // Track repository calls in traced methods
572
+ CallExpression(node) {
573
+ // Check repository method calls
574
+ const methodName = node.callee?.property?.name;
575
+ if (["find", "findOne", "findOneBy", "findAndCount", "findOneOrFail"].includes(methodName)) {
576
+ const entityName = getEntityFromRepositoryCall(node);
577
+
578
+ if (entityName) {
579
+ const requestedRelations = getRelationsFromFindOptions(node);
580
+ // Load relations from external entity files if not in current file
581
+ const availableRelations = loadEntityRelations(entityName);
582
+
583
+ // Validate each requested relation against available relations
584
+ requestedRelations.forEach(relation => {
585
+ // Check if it's a nested relation (contains dots)
586
+ if (relation.includes('.')) {
587
+ // Validate nested relation
588
+ if (!validateNestedRelation(entityName, relation)) {
589
+ context.report({
590
+ node,
591
+ messageId: "invalidRelation",
592
+ data: {
593
+ relationName: relation,
594
+ entityName,
595
+ availableRelations: availableRelations.join(", ") || "none",
596
+ },
597
+ });
598
+ }
599
+ } else {
600
+ // Simple relation validation
601
+ if (!availableRelations.includes(relation)) {
602
+ context.report({
603
+ node,
604
+ messageId: "invalidRelation",
605
+ data: {
606
+ relationName: relation,
607
+ entityName,
608
+ availableRelations: availableRelations.join(", ") || "none",
609
+ },
610
+ });
611
+ }
612
+ }
613
+ });
614
+
615
+ // Store for further analysis
616
+ if (currentMethod) {
617
+ if (!serviceMethodCalls.has(currentMethod)) {
618
+ serviceMethodCalls.set(currentMethod, []);
619
+ }
620
+
621
+ serviceMethodCalls.get(currentMethod).push({
622
+ node,
623
+ entityName,
624
+ relations: requestedRelations,
625
+ });
626
+ }
627
+ }
628
+ }
629
+
630
+ // Track DTO usage
631
+ if (currentMethod) {
632
+ trackDtoUsage(node, currentMethod);
633
+ }
634
+ },
635
+
636
+ // Perform final analysis when processing is complete
637
+ "Program:exit"() {
638
+ serviceMethodCalls.forEach((repositoryCalls, methodName) => {
639
+ const usedDtos = dataFlow.get(methodName) || new Set();
640
+
641
+ usedDtos.forEach(dtoName => {
642
+ const dtoProperties = dtoPropertyAccess.get(dtoName) || new Set();
643
+
644
+ repositoryCalls.forEach(repoCall => {
645
+ const entityName = repoCall.entityName;
646
+ const loadedRelations = repoCall.relations;
647
+ const entityRelationsList = loadEntityRelations(entityName);
648
+
649
+ // Find accessed properties that are relations but not loaded
650
+ const accessedRelations = [...dtoProperties].filter(prop =>
651
+ entityRelationsList.includes(prop)
652
+ );
653
+
654
+ const missingRelations = accessedRelations.filter(rel =>
655
+ !loadedRelations.includes(rel)
656
+ );
657
+
658
+ if (missingRelations.length > 0) {
659
+ context.report({
660
+ node: repoCall.node,
661
+ messageId: "suggestRelationLoad",
662
+ data: {
663
+ methodName,
664
+ suggestedRelations: missingRelations.join('", "'),
665
+ },
666
+ });
667
+ }
668
+ });
669
+ });
670
+ });
671
+
672
+ // Clear state for next file
673
+ visitedNodes.clear();
674
+ },
675
+
676
+ Program() {
677
+ // Reset state at the start of each file
678
+ visitedNodes.clear();
679
+ serviceMethodCalls.clear();
680
+ dataFlow.clear();
681
+ currentMethod = null;
682
+ },
683
+ };
684
+ }
685
+ };
686
+
687
+ export default analyzeRelationUsageRule;