@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,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;
|