@nest-boot/eslint-plugin 7.0.3 → 7.0.5
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/.turbo/turbo-build.log +1 -1
- package/CHANGELOG.md +12 -0
- package/dist/rules/graphql/graphql-field-config-from-types.d.ts +3 -1
- package/dist/rules/graphql/graphql-field-config-from-types.js +36 -36
- package/dist/rules/graphql/graphql-field-config-from-types.js.map +1 -1
- package/dist/rules/graphql/graphql-field-definite-assignment.d.ts +3 -1
- package/dist/rules/graphql/graphql-field-definite-assignment.js +13 -13
- package/dist/rules/graphql/graphql-field-definite-assignment.js.map +1 -1
- package/dist/rules/import/import-bullmq.d.ts +3 -1
- package/dist/rules/import/import-bullmq.js +4 -4
- package/dist/rules/import/import-bullmq.js.map +1 -1
- package/dist/rules/import/import-graphql.d.ts +3 -1
- package/dist/rules/import/import-graphql.js +4 -4
- package/dist/rules/import/import-graphql.js.map +1 -1
- package/dist/rules/import/import-mikro-orm.d.ts +3 -1
- package/dist/rules/import/import-mikro-orm.js +4 -4
- package/dist/rules/import/import-mikro-orm.js.map +1 -1
- package/dist/rules/index.d.ts +21 -7
- package/dist/rules/mikro-orm/entity-field-definite-assignment.d.ts +3 -1
- package/dist/rules/mikro-orm/entity-field-definite-assignment.js +12 -10
- package/dist/rules/mikro-orm/entity-field-definite-assignment.js.map +1 -1
- package/dist/rules/mikro-orm/entity-property-config-from-types.d.ts +3 -1
- package/dist/rules/mikro-orm/entity-property-config-from-types.js +149 -116
- package/dist/rules/mikro-orm/entity-property-config-from-types.js.map +1 -1
- package/dist/tsconfig.build.tsbuildinfo +1 -1
- package/dist/utils/createRule.d.ts +3 -1
- package/dist/utils/decorators.d.ts +16 -16
- package/dist/utils/decorators.js +16 -16
- package/package.json +12 -7
- package/src/rules/graphql/graphql-field-config-from-types.spec.ts +18 -18
- package/src/rules/graphql/graphql-field-config-from-types.ts +37 -37
- package/src/rules/graphql/graphql-field-definite-assignment.spec.ts +11 -11
- package/src/rules/graphql/graphql-field-definite-assignment.ts +13 -13
- package/src/rules/import/import-bullmq.spec.ts +9 -9
- package/src/rules/import/import-bullmq.ts +5 -4
- package/src/rules/import/import-graphql.spec.ts +8 -8
- package/src/rules/import/import-graphql.ts +4 -4
- package/src/rules/import/import-mikro-orm.spec.ts +8 -8
- package/src/rules/import/import-mikro-orm.ts +4 -4
- package/src/rules/mikro-orm/entity-field-definite-assignment.spec.ts +18 -18
- package/src/rules/mikro-orm/entity-field-definite-assignment.ts +12 -10
- package/src/rules/mikro-orm/entity-property-config-from-types.spec.ts +24 -24
- package/src/rules/mikro-orm/entity-property-config-from-types.ts +171 -117
- package/src/utils/decorators.ts +16 -16
|
@@ -42,16 +42,16 @@ exports.default = (0, createRule_1.createRule)({
|
|
|
42
42
|
meta: {
|
|
43
43
|
type: "problem",
|
|
44
44
|
docs: {
|
|
45
|
-
description: "
|
|
45
|
+
description: "Automatically generate or fix @Property decorator type and nullable configuration based on TypeScript types (with array support), as well as @Enum decorator nullable configuration. Checks whether properties with initializers use the Opt<T> type.",
|
|
46
46
|
},
|
|
47
47
|
fixable: "code",
|
|
48
48
|
schema: [],
|
|
49
49
|
messages: {
|
|
50
|
-
alignPropertyDecoratorWithTsType: "@Property
|
|
51
|
-
removePropertyDecorator: "
|
|
52
|
-
useEnumDecorator: "
|
|
53
|
-
useOptTypeForInitializedProperty: "
|
|
54
|
-
removeOptTypeForNonInitializedProperty: "
|
|
50
|
+
alignPropertyDecoratorWithTsType: "@Property decorator should align with the TypeScript type (type and nullable).",
|
|
51
|
+
removePropertyDecorator: "Property has a @{{decoratorName}} decorator, @Property decorator should be removed.",
|
|
52
|
+
useEnumDecorator: "Enum types should use @Enum decorator instead of @Property decorator.",
|
|
53
|
+
useOptTypeForInitializedProperty: "Properties with non-null initializers should use the Opt<T> type wrapper.",
|
|
54
|
+
removeOptTypeForNonInitializedProperty: "Properties without initializers or initialized to null should not use the Opt<T> type wrapper.",
|
|
55
55
|
},
|
|
56
56
|
},
|
|
57
57
|
defaultOptions: [],
|
|
@@ -59,16 +59,16 @@ exports.default = (0, createRule_1.createRule)({
|
|
|
59
59
|
const source = context.sourceCode;
|
|
60
60
|
const parserServices = utils_1.ESLintUtils.getParserServices(context);
|
|
61
61
|
const checker = parserServices.program.getTypeChecker();
|
|
62
|
-
//
|
|
62
|
+
// Check if the type is an enum
|
|
63
63
|
const isEnumType = (node) => {
|
|
64
64
|
try {
|
|
65
65
|
const tsNode = parserServices.esTreeNodeToTSNodeMap.get(node);
|
|
66
66
|
const type = checker.getTypeAtLocation(tsNode);
|
|
67
|
-
//
|
|
67
|
+
// Check if it is an enum type
|
|
68
68
|
if (type.symbol.flags & ts.SymbolFlags.Enum) {
|
|
69
69
|
return true;
|
|
70
70
|
}
|
|
71
|
-
//
|
|
71
|
+
// Check each member in a union type
|
|
72
72
|
if (type.isUnion()) {
|
|
73
73
|
return type.types.some((t) => t.symbol.flags & ts.SymbolFlags.EnumMember ||
|
|
74
74
|
t.symbol.flags & ts.SymbolFlags.Enum);
|
|
@@ -85,15 +85,15 @@ exports.default = (0, createRule_1.createRule)({
|
|
|
85
85
|
switch (typeName) {
|
|
86
86
|
case "String":
|
|
87
87
|
case "string":
|
|
88
|
-
return "t.string"; //
|
|
88
|
+
return "t.string"; // Default to t.string
|
|
89
89
|
case "Number":
|
|
90
90
|
case "number":
|
|
91
|
-
return "t.float"; //
|
|
91
|
+
return "t.float"; // Default to t.float
|
|
92
92
|
case "Boolean":
|
|
93
93
|
case "boolean":
|
|
94
|
-
return "t.boolean"; //
|
|
94
|
+
return "t.boolean"; // Default to t.boolean
|
|
95
95
|
case "Date":
|
|
96
|
-
return "t.datetime"; //
|
|
96
|
+
return "t.datetime"; // Default to t.datetime
|
|
97
97
|
case "GraphQLJSONObject":
|
|
98
98
|
case "Record":
|
|
99
99
|
return "t.json";
|
|
@@ -104,27 +104,28 @@ exports.default = (0, createRule_1.createRule)({
|
|
|
104
104
|
const isValidStringType = (typeConfig) => {
|
|
105
105
|
if (!typeConfig)
|
|
106
106
|
return false;
|
|
107
|
-
//
|
|
107
|
+
// Accept t.string, t.text, t.uuid, t.decimal, t.bigint
|
|
108
108
|
if (typeConfig === "t.string" ||
|
|
109
109
|
typeConfig === "t.text" ||
|
|
110
|
+
typeConfig === "t.uuid" ||
|
|
110
111
|
typeConfig === "t.decimal" ||
|
|
111
112
|
typeConfig === "t.bigint") {
|
|
112
113
|
return true;
|
|
113
114
|
}
|
|
114
|
-
//
|
|
115
|
+
// Accept DecimalType (class reference, without arguments)
|
|
115
116
|
if (typeConfig === "DecimalType") {
|
|
116
117
|
return true;
|
|
117
118
|
}
|
|
118
|
-
//
|
|
119
|
-
//
|
|
119
|
+
// Accept new DecimalType('string') and new BigIntType('string')
|
|
120
|
+
// but exclude new BigIntType('number')
|
|
120
121
|
if (typeConfig.includes("DecimalType") ||
|
|
121
122
|
typeConfig.includes("BigIntType")) {
|
|
122
|
-
//
|
|
123
|
+
// If it's new BigIntType('number'), it's invalid
|
|
123
124
|
if (typeConfig.includes("BigIntType") &&
|
|
124
125
|
(typeConfig.includes("'number'") || typeConfig.includes('"number"'))) {
|
|
125
126
|
return false;
|
|
126
127
|
}
|
|
127
|
-
//
|
|
128
|
+
// Other cases (including new XXXType('string') and DecimalType) are valid
|
|
128
129
|
return true;
|
|
129
130
|
}
|
|
130
131
|
return false;
|
|
@@ -132,27 +133,27 @@ exports.default = (0, createRule_1.createRule)({
|
|
|
132
133
|
const isValidNumberType = (typeConfig) => {
|
|
133
134
|
if (!typeConfig)
|
|
134
135
|
return false;
|
|
135
|
-
//
|
|
136
|
+
// Accept t.integer, t.float, t.double, t.decimal
|
|
136
137
|
if (typeConfig === "t.integer" ||
|
|
137
138
|
typeConfig === "t.float" ||
|
|
138
139
|
typeConfig === "t.double" ||
|
|
139
140
|
typeConfig === "t.decimal") {
|
|
140
141
|
return true;
|
|
141
142
|
}
|
|
142
|
-
//
|
|
143
|
+
// Accept BigIntType and DecimalType (class reference, without arguments)
|
|
143
144
|
if (typeConfig === "BigIntType" || typeConfig === "DecimalType") {
|
|
144
145
|
return true;
|
|
145
146
|
}
|
|
146
|
-
//
|
|
147
|
-
//
|
|
147
|
+
// Accept new DecimalType('number') and new BigIntType('number')
|
|
148
|
+
// but exclude new XXXType('string')
|
|
148
149
|
if (typeConfig.includes("DecimalType") ||
|
|
149
150
|
typeConfig.includes("BigIntType")) {
|
|
150
|
-
//
|
|
151
|
+
// If it contains 'string', it's invalid for number type
|
|
151
152
|
if (typeConfig.includes("'string'") ||
|
|
152
153
|
typeConfig.includes('"string"')) {
|
|
153
154
|
return false;
|
|
154
155
|
}
|
|
155
|
-
//
|
|
156
|
+
// Other cases (including new XXXType('number')) are valid
|
|
156
157
|
return true;
|
|
157
158
|
}
|
|
158
159
|
return false;
|
|
@@ -160,15 +161,15 @@ exports.default = (0, createRule_1.createRule)({
|
|
|
160
161
|
const isValidNumberArrayType = (typeConfig) => {
|
|
161
162
|
if (!typeConfig)
|
|
162
163
|
return false;
|
|
163
|
-
//
|
|
164
|
+
// Accept t.array (default)
|
|
164
165
|
if (typeConfig === "t.array") {
|
|
165
166
|
return true;
|
|
166
167
|
}
|
|
167
|
-
//
|
|
168
|
+
// Accept VectorType (class reference)
|
|
168
169
|
if (typeConfig === "VectorType") {
|
|
169
170
|
return true;
|
|
170
171
|
}
|
|
171
|
-
//
|
|
172
|
+
// Accept new VectorType(...)
|
|
172
173
|
if (typeConfig.includes("VectorType")) {
|
|
173
174
|
return true;
|
|
174
175
|
}
|
|
@@ -177,7 +178,7 @@ exports.default = (0, createRule_1.createRule)({
|
|
|
177
178
|
const isValidDateType = (typeConfig) => {
|
|
178
179
|
if (!typeConfig)
|
|
179
180
|
return false;
|
|
180
|
-
//
|
|
181
|
+
// Accept t.datetime, t.date, t.time
|
|
181
182
|
if (typeConfig === "t.datetime" ||
|
|
182
183
|
typeConfig === "t.date" ||
|
|
183
184
|
typeConfig === "t.time") {
|
|
@@ -208,7 +209,7 @@ exports.default = (0, createRule_1.createRule)({
|
|
|
208
209
|
node.typeName.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
209
210
|
node.typeName.name === "Collection");
|
|
210
211
|
};
|
|
211
|
-
//
|
|
212
|
+
// Check if the type is wrapped with Opt<T>
|
|
212
213
|
const isWrappedWithOpt = (property) => {
|
|
213
214
|
const typeAnnotation = property.typeAnnotation;
|
|
214
215
|
if (typeAnnotation?.type !== utils_1.AST_NODE_TYPES.TSTypeAnnotation) {
|
|
@@ -219,13 +220,13 @@ exports.default = (0, createRule_1.createRule)({
|
|
|
219
220
|
typeNode.typeName.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
220
221
|
typeNode.typeName.name === "Opt");
|
|
221
222
|
};
|
|
222
|
-
//
|
|
223
|
+
// Check if Opt is imported
|
|
223
224
|
const hasOptImport = () => {
|
|
224
225
|
const program = context.sourceCode.ast;
|
|
225
226
|
for (const statement of program.body) {
|
|
226
227
|
if (statement.type === utils_1.AST_NODE_TYPES.ImportDeclaration) {
|
|
227
228
|
const importSource = statement.source.value;
|
|
228
|
-
//
|
|
229
|
+
// Check imports from @mikro-orm/*
|
|
229
230
|
if (typeof importSource === "string" &&
|
|
230
231
|
importSource.startsWith("@mikro-orm/")) {
|
|
231
232
|
const hasOpt = statement.specifiers.some((spec) => {
|
|
@@ -240,10 +241,10 @@ exports.default = (0, createRule_1.createRule)({
|
|
|
240
241
|
}
|
|
241
242
|
return false;
|
|
242
243
|
};
|
|
243
|
-
//
|
|
244
|
+
// Add Opt to the @mikro-orm/core import
|
|
244
245
|
const addOptImport = (fixer) => {
|
|
245
246
|
const program = context.sourceCode.ast;
|
|
246
|
-
//
|
|
247
|
+
// Find the @mikro-orm/core import statement
|
|
247
248
|
let coreImport = null;
|
|
248
249
|
for (const statement of program.body) {
|
|
249
250
|
if (statement.type === utils_1.AST_NODE_TYPES.ImportDeclaration) {
|
|
@@ -255,23 +256,23 @@ exports.default = (0, createRule_1.createRule)({
|
|
|
255
256
|
}
|
|
256
257
|
}
|
|
257
258
|
if (coreImport) {
|
|
258
|
-
//
|
|
259
|
+
// Already has @mikro-orm/core import, add Opt to the import list
|
|
259
260
|
const lastSpecifier = coreImport.specifiers[coreImport.specifiers.length - 1];
|
|
260
|
-
//
|
|
261
|
+
// Check if it's a multiline import
|
|
261
262
|
const importText = source.getText(coreImport);
|
|
262
263
|
const isMultiline = importText.includes("\n");
|
|
263
264
|
if (isMultiline) {
|
|
264
|
-
//
|
|
265
|
-
const indent = " "; //
|
|
265
|
+
// Multiline import: add after the last import item, keeping indentation
|
|
266
|
+
const indent = " "; // Assuming 2-space indentation
|
|
266
267
|
return fixer.insertTextAfter(lastSpecifier, `,\n${indent}Opt`);
|
|
267
268
|
}
|
|
268
269
|
else {
|
|
269
|
-
//
|
|
270
|
+
// Single-line import: add directly
|
|
270
271
|
return fixer.insertTextAfter(lastSpecifier, ", Opt");
|
|
271
272
|
}
|
|
272
273
|
}
|
|
273
274
|
else {
|
|
274
|
-
//
|
|
275
|
+
// No @mikro-orm/core import, add a new import statement at the top
|
|
275
276
|
const firstImport = program.body.find((node) => node.type === utils_1.AST_NODE_TYPES.ImportDeclaration);
|
|
276
277
|
if (firstImport) {
|
|
277
278
|
return fixer.insertTextBefore(firstImport, "import { Opt } from '@mikro-orm/core';\n");
|
|
@@ -286,11 +287,11 @@ exports.default = (0, createRule_1.createRule)({
|
|
|
286
287
|
let baseTypeNode = property.typeAnnotation?.type === utils_1.AST_NODE_TYPES.TSTypeAnnotation
|
|
287
288
|
? property.typeAnnotation.typeAnnotation
|
|
288
289
|
: null;
|
|
289
|
-
//
|
|
290
|
+
// Optional property (?) is treated as nullable
|
|
290
291
|
if (property.optional) {
|
|
291
292
|
isNullable = true;
|
|
292
293
|
}
|
|
293
|
-
//
|
|
294
|
+
// Handle null/undefined in union types
|
|
294
295
|
if (baseTypeNode?.type === utils_1.AST_NODE_TYPES.TSUnionType) {
|
|
295
296
|
const hasNullish = baseTypeNode.types.some((t) => {
|
|
296
297
|
return (t.type === utils_1.AST_NODE_TYPES.TSNullKeyword ||
|
|
@@ -304,7 +305,7 @@ exports.default = (0, createRule_1.createRule)({
|
|
|
304
305
|
t.type !== utils_1.AST_NODE_TYPES.TSUndefinedKeyword);
|
|
305
306
|
}) ?? null;
|
|
306
307
|
}
|
|
307
|
-
//
|
|
308
|
+
// First unwrap Ref<T> and Opt<T> → T, and handle null/undefined within
|
|
308
309
|
if (baseTypeNode?.type === utils_1.AST_NODE_TYPES.TSTypeReference &&
|
|
309
310
|
baseTypeNode.typeName.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
310
311
|
(baseTypeNode.typeName.name === "Ref" ||
|
|
@@ -325,11 +326,11 @@ exports.default = (0, createRule_1.createRule)({
|
|
|
325
326
|
}
|
|
326
327
|
baseTypeNode = inner ?? baseTypeNode;
|
|
327
328
|
}
|
|
328
|
-
//
|
|
329
|
+
// Skip Collection<T> types (these should be handled by OneToMany etc. decorators)
|
|
329
330
|
if (baseTypeNode && isCollectionType(baseTypeNode)) {
|
|
330
331
|
return null;
|
|
331
332
|
}
|
|
332
|
-
//
|
|
333
|
+
// Array type (T[] or Array<T>) - checked after unwrapping Opt/Ref
|
|
333
334
|
const elementTypeNode = baseTypeNode
|
|
334
335
|
? extractArrayElementType(baseTypeNode)
|
|
335
336
|
: null;
|
|
@@ -337,11 +338,11 @@ exports.default = (0, createRule_1.createRule)({
|
|
|
337
338
|
isArray = true;
|
|
338
339
|
}
|
|
339
340
|
const targetTypeNode = elementTypeNode ?? baseTypeNode;
|
|
340
|
-
//
|
|
341
|
+
// Check if target type is an enum
|
|
341
342
|
if (targetTypeNode) {
|
|
342
343
|
isEnum = isEnumType(targetTypeNode);
|
|
343
344
|
}
|
|
344
|
-
//
|
|
345
|
+
// When no explicit type, try to infer from literal initializer
|
|
345
346
|
if (!targetTypeNode) {
|
|
346
347
|
if (property.value?.type === utils_1.AST_NODE_TYPES.Literal) {
|
|
347
348
|
const value = property.value.value;
|
|
@@ -389,7 +390,7 @@ exports.default = (0, createRule_1.createRule)({
|
|
|
389
390
|
arrayElementTypeName: isArray ? "Record" : undefined,
|
|
390
391
|
};
|
|
391
392
|
}
|
|
392
|
-
//
|
|
393
|
+
// Keyword types
|
|
393
394
|
if (targetTypeNode.type === utils_1.AST_NODE_TYPES.TSStringKeyword) {
|
|
394
395
|
return {
|
|
395
396
|
typeName: "string",
|
|
@@ -420,10 +421,10 @@ exports.default = (0, createRule_1.createRule)({
|
|
|
420
421
|
arrayElementTypeName: isArray ? "boolean" : undefined,
|
|
421
422
|
};
|
|
422
423
|
}
|
|
423
|
-
//
|
|
424
|
+
// Identifier (class/custom type)
|
|
424
425
|
const ident = getIdentifierName(targetTypeNode);
|
|
425
426
|
if (ident) {
|
|
426
|
-
//
|
|
427
|
+
// For arrays, custom type arrays use t.json
|
|
427
428
|
if (isArray) {
|
|
428
429
|
return {
|
|
429
430
|
typeName: ident,
|
|
@@ -445,7 +446,7 @@ exports.default = (0, createRule_1.createRule)({
|
|
|
445
446
|
}
|
|
446
447
|
return null;
|
|
447
448
|
};
|
|
448
|
-
//
|
|
449
|
+
// Wrap type with Opt<T>
|
|
449
450
|
const wrapWithOpt = (typeString) => {
|
|
450
451
|
return `Opt<${typeString}>`;
|
|
451
452
|
};
|
|
@@ -462,13 +463,13 @@ exports.default = (0, createRule_1.createRule)({
|
|
|
462
463
|
}
|
|
463
464
|
return `@Enum({ ${options.join(", ")} })`;
|
|
464
465
|
};
|
|
465
|
-
const buildPropertyDecorator = (info, otherProps = []) => {
|
|
466
|
+
const buildPropertyDecorator = (info, otherProps = [], decoratorName = "Property") => {
|
|
466
467
|
const options = [];
|
|
467
|
-
//
|
|
468
|
+
// If there is a propertyType configuration, add type
|
|
468
469
|
if (info.propertyType) {
|
|
469
470
|
options.push(`type: ${info.propertyType}`);
|
|
470
471
|
}
|
|
471
|
-
//
|
|
472
|
+
// Add other properties (keeping original order)
|
|
472
473
|
for (const prop of otherProps) {
|
|
473
474
|
options.push(`${prop.key}: ${prop.value}`);
|
|
474
475
|
}
|
|
@@ -476,9 +477,9 @@ exports.default = (0, createRule_1.createRule)({
|
|
|
476
477
|
options.push("nullable: true");
|
|
477
478
|
}
|
|
478
479
|
if (options.length === 0) {
|
|
479
|
-
return
|
|
480
|
+
return `@${decoratorName}()`;
|
|
480
481
|
}
|
|
481
|
-
return
|
|
482
|
+
return `@${decoratorName}({ ${options.join(", ")} })`;
|
|
482
483
|
};
|
|
483
484
|
const addPropertyDecorator = (property, info) => {
|
|
484
485
|
const fixes = [];
|
|
@@ -490,33 +491,39 @@ exports.default = (0, createRule_1.createRule)({
|
|
|
490
491
|
});
|
|
491
492
|
return fixes;
|
|
492
493
|
};
|
|
493
|
-
const fixWithTypeInfo = (property, propertyDecorator, info, currentConfig) => {
|
|
494
|
+
const fixWithTypeInfo = (property, propertyDecorator, info, currentConfig, decoratorName = "Property") => {
|
|
494
495
|
const fixes = [];
|
|
495
|
-
//
|
|
496
|
+
// If current config already has a valid value, keep it instead of replacing with default
|
|
496
497
|
const finalInfo = { ...info };
|
|
498
|
+
// PrimaryKey number type defaults to t.integer
|
|
499
|
+
if (decoratorName === "PrimaryKey" &&
|
|
500
|
+
finalInfo.propertyType === "t.float") {
|
|
501
|
+
finalInfo.propertyType = "t.integer";
|
|
502
|
+
}
|
|
497
503
|
if (info.propertyType === "t.string" &&
|
|
498
504
|
isValidStringType(currentConfig.type)) {
|
|
499
|
-
//
|
|
505
|
+
// Keep valid string type config
|
|
500
506
|
finalInfo.propertyType = currentConfig.type;
|
|
501
507
|
}
|
|
502
|
-
else if (info.propertyType === "t.float"
|
|
508
|
+
else if ((info.propertyType === "t.float" ||
|
|
509
|
+
info.propertyType === "t.integer") &&
|
|
503
510
|
isValidNumberType(currentConfig.type)) {
|
|
504
|
-
//
|
|
511
|
+
// Keep valid number type config
|
|
505
512
|
finalInfo.propertyType = currentConfig.type;
|
|
506
513
|
}
|
|
507
514
|
else if (info.propertyType === "t.datetime" &&
|
|
508
515
|
isValidDateType(currentConfig.type)) {
|
|
509
|
-
//
|
|
516
|
+
// Keep valid Date type config
|
|
510
517
|
finalInfo.propertyType = currentConfig.type;
|
|
511
518
|
}
|
|
512
519
|
else if (info.isArray &&
|
|
513
520
|
info.arrayElementTypeName === "number" &&
|
|
514
521
|
isValidNumberArrayType(currentConfig.type)) {
|
|
515
|
-
//
|
|
522
|
+
// Keep valid number[] type config (VectorType)
|
|
516
523
|
finalInfo.propertyType = currentConfig.type;
|
|
517
524
|
}
|
|
518
|
-
//
|
|
519
|
-
const newDecoratorText = buildPropertyDecorator(finalInfo, currentConfig.otherProps);
|
|
525
|
+
// Keep other property configs
|
|
526
|
+
const newDecoratorText = buildPropertyDecorator(finalInfo, currentConfig.otherProps, decoratorName);
|
|
520
527
|
fixes.push({
|
|
521
528
|
type: "replace",
|
|
522
529
|
range: propertyDecorator.range,
|
|
@@ -565,7 +572,7 @@ exports.default = (0, createRule_1.createRule)({
|
|
|
565
572
|
}
|
|
566
573
|
}
|
|
567
574
|
else {
|
|
568
|
-
//
|
|
575
|
+
// Keep all other properties
|
|
569
576
|
otherProps.push({
|
|
570
577
|
key: prop.key.name,
|
|
571
578
|
value: source.getText(prop.value),
|
|
@@ -603,38 +610,38 @@ exports.default = (0, createRule_1.createRule)({
|
|
|
603
610
|
}
|
|
604
611
|
return { items, nullable };
|
|
605
612
|
};
|
|
606
|
-
//
|
|
613
|
+
// Check if the file uses the Opt type but hasn't imported it
|
|
607
614
|
const checkOptUsageWithoutImport = (node) => {
|
|
608
615
|
let usesOpt = false;
|
|
609
|
-
//
|
|
616
|
+
// Iterate through all members to check for Opt<T> type annotations
|
|
610
617
|
node.body.body.forEach((member) => {
|
|
611
618
|
if (member.type !== utils_1.AST_NODE_TYPES.PropertyDefinition)
|
|
612
619
|
return;
|
|
613
620
|
const typeAnnotation = member.typeAnnotation?.typeAnnotation;
|
|
614
621
|
if (!typeAnnotation)
|
|
615
622
|
return;
|
|
616
|
-
//
|
|
623
|
+
// Recursively check if the type node contains Opt
|
|
617
624
|
const containsOpt = (typeNode) => {
|
|
618
625
|
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
|
619
626
|
if (!typeNode)
|
|
620
627
|
return false;
|
|
621
|
-
//
|
|
628
|
+
// Check if it's Opt<T>
|
|
622
629
|
if (typeNode.type === utils_1.AST_NODE_TYPES.TSTypeReference &&
|
|
623
630
|
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
|
624
631
|
typeNode.typeName?.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
625
632
|
typeNode.typeName.name === "Opt") {
|
|
626
633
|
return true;
|
|
627
634
|
}
|
|
628
|
-
//
|
|
635
|
+
// Recursively check union types
|
|
629
636
|
if (typeNode.type === utils_1.AST_NODE_TYPES.TSUnionType) {
|
|
630
637
|
return typeNode.types.some((t) => containsOpt(t));
|
|
631
638
|
}
|
|
632
|
-
//
|
|
639
|
+
// Recursively check type parameters
|
|
633
640
|
if (typeNode.type === utils_1.AST_NODE_TYPES.TSTypeReference &&
|
|
634
641
|
typeNode.typeArguments?.params) {
|
|
635
642
|
return typeNode.typeArguments.params.some((param) => containsOpt(param));
|
|
636
643
|
}
|
|
637
|
-
//
|
|
644
|
+
// Recursively check array element type
|
|
638
645
|
if (typeNode.type === utils_1.AST_NODE_TYPES.TSArrayType) {
|
|
639
646
|
return containsOpt(typeNode.elementType);
|
|
640
647
|
}
|
|
@@ -644,7 +651,7 @@ exports.default = (0, createRule_1.createRule)({
|
|
|
644
651
|
usesOpt = true;
|
|
645
652
|
}
|
|
646
653
|
});
|
|
647
|
-
//
|
|
654
|
+
// If Opt is used but not imported, report an error
|
|
648
655
|
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
|
649
656
|
if (usesOpt && !hasOptImport()) {
|
|
650
657
|
context.report({
|
|
@@ -661,19 +668,18 @@ exports.default = (0, createRule_1.createRule)({
|
|
|
661
668
|
ClassDeclaration(node) {
|
|
662
669
|
if (!isEntityClass(node))
|
|
663
670
|
return;
|
|
664
|
-
//
|
|
671
|
+
// First check if Opt is used but not imported
|
|
665
672
|
checkOptUsageWithoutImport(node);
|
|
666
673
|
node.body.body.forEach((member) => {
|
|
667
674
|
if (member.type !== utils_1.AST_NODE_TYPES.PropertyDefinition)
|
|
668
675
|
return;
|
|
669
|
-
//
|
|
676
|
+
// Check relation decorators (OneToOne, OneToMany, ManyToOne, ManyToMany)
|
|
670
677
|
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
|
671
678
|
const hasRelationDecorator = member.decorators?.some((decorator) => {
|
|
672
679
|
if (decorator.expression.type === utils_1.AST_NODE_TYPES.CallExpression &&
|
|
673
680
|
decorator.expression.callee.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
674
681
|
const decoratorName = decorator.expression.callee.name;
|
|
675
682
|
return [
|
|
676
|
-
"PrimaryKey",
|
|
677
683
|
"OneToOne",
|
|
678
684
|
"OneToMany",
|
|
679
685
|
"ManyToOne",
|
|
@@ -682,41 +688,63 @@ exports.default = (0, createRule_1.createRule)({
|
|
|
682
688
|
}
|
|
683
689
|
return false;
|
|
684
690
|
});
|
|
685
|
-
//
|
|
691
|
+
// If there are relation decorators, skip checking (these properties don't need @Property)
|
|
686
692
|
if (hasRelationDecorator)
|
|
687
693
|
return;
|
|
688
|
-
//
|
|
694
|
+
// Property-like decorators (decorators that need type checking)
|
|
695
|
+
const propertyLikeDecorators = [
|
|
696
|
+
"Property",
|
|
697
|
+
"PrimaryKey",
|
|
698
|
+
"EncryptedProperty",
|
|
699
|
+
"HashedProperty",
|
|
700
|
+
];
|
|
701
|
+
// Find property-like decorator
|
|
689
702
|
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
|
690
|
-
const
|
|
691
|
-
|
|
692
|
-
decorator.expression.callee.type ===
|
|
693
|
-
|
|
694
|
-
|
|
703
|
+
const propertyLikeDecorator = member.decorators?.find((decorator) => {
|
|
704
|
+
if (decorator.expression.type === utils_1.AST_NODE_TYPES.CallExpression &&
|
|
705
|
+
decorator.expression.callee.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
706
|
+
return propertyLikeDecorators.includes(decorator.expression.callee.name);
|
|
707
|
+
}
|
|
708
|
+
return false;
|
|
695
709
|
});
|
|
710
|
+
// Get decorator name
|
|
711
|
+
const getDecoratorName = (decorator) => {
|
|
712
|
+
if (decorator.expression.type === utils_1.AST_NODE_TYPES.CallExpression &&
|
|
713
|
+
decorator.expression.callee.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
714
|
+
return decorator.expression.callee.name;
|
|
715
|
+
}
|
|
716
|
+
return null;
|
|
717
|
+
};
|
|
718
|
+
// Check @Enum decorator
|
|
696
719
|
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
|
697
|
-
const
|
|
720
|
+
const enumDecorator = member.decorators?.find((decorator) => {
|
|
698
721
|
return (decorator.expression.type === utils_1.AST_NODE_TYPES.CallExpression &&
|
|
699
722
|
decorator.expression.callee.type ===
|
|
700
723
|
utils_1.AST_NODE_TYPES.Identifier &&
|
|
701
|
-
decorator.expression.callee.name === "
|
|
724
|
+
decorator.expression.callee.name === "Enum");
|
|
702
725
|
});
|
|
726
|
+
// Use property-like decorator
|
|
727
|
+
const propertyDecorator = propertyLikeDecorator;
|
|
728
|
+
const currentDecoratorName = propertyDecorator
|
|
729
|
+
? getDecoratorName(propertyDecorator)
|
|
730
|
+
: null;
|
|
703
731
|
const typeInfo = computeTypeInfo(member);
|
|
704
732
|
if (!typeInfo?.typeName)
|
|
705
733
|
return;
|
|
706
|
-
//
|
|
734
|
+
// Check initializer and Opt<T> type match
|
|
707
735
|
const hasInitializer = member.value !== null;
|
|
708
736
|
const isOptWrapped = isWrappedWithOpt(member);
|
|
709
|
-
//
|
|
737
|
+
// Check if the initializer is null
|
|
710
738
|
const isInitializedToNull = hasInitializer &&
|
|
711
739
|
member.value?.type === utils_1.AST_NODE_TYPES.Literal &&
|
|
712
740
|
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
|
713
741
|
member.value?.value === null;
|
|
714
|
-
//
|
|
742
|
+
// Case 1: Has non-null initializer but not wrapped with Opt<T>
|
|
715
743
|
if (hasInitializer &&
|
|
716
744
|
!isInitializedToNull &&
|
|
717
745
|
!isOptWrapped &&
|
|
718
746
|
!member.optional) {
|
|
719
|
-
//
|
|
747
|
+
// Has initializer but not wrapped with Opt<T>, needs to report error
|
|
720
748
|
const typeAnnotation = member.typeAnnotation?.typeAnnotation;
|
|
721
749
|
const needsImport = !hasOptImport();
|
|
722
750
|
if (typeAnnotation) {
|
|
@@ -729,7 +757,7 @@ exports.default = (0, createRule_1.createRule)({
|
|
|
729
757
|
const fixes = [
|
|
730
758
|
fixer.replaceText(typeAnnotation, wrappedType),
|
|
731
759
|
];
|
|
732
|
-
//
|
|
760
|
+
// If needed, add Opt import
|
|
733
761
|
if (needsImport) {
|
|
734
762
|
const importFix = addOptImport(fixer);
|
|
735
763
|
if (importFix)
|
|
@@ -740,7 +768,7 @@ exports.default = (0, createRule_1.createRule)({
|
|
|
740
768
|
});
|
|
741
769
|
}
|
|
742
770
|
else if (member.value) {
|
|
743
|
-
//
|
|
771
|
+
// No type annotation, infer type from initializer
|
|
744
772
|
let inferredType = null;
|
|
745
773
|
if (member.value.type === utils_1.AST_NODE_TYPES.Literal) {
|
|
746
774
|
const valueType = typeof member.value.value;
|
|
@@ -752,11 +780,11 @@ exports.default = (0, createRule_1.createRule)({
|
|
|
752
780
|
inferredType = "string";
|
|
753
781
|
}
|
|
754
782
|
else if (member.value.type === utils_1.AST_NODE_TYPES.ArrayExpression) {
|
|
755
|
-
//
|
|
783
|
+
// Empty array [] case, need to infer type from @Property decorator
|
|
756
784
|
inferredType = "unknown[]";
|
|
757
785
|
}
|
|
758
786
|
else if (member.value.type === utils_1.AST_NODE_TYPES.NewExpression) {
|
|
759
|
-
// new Date()
|
|
787
|
+
// new Date() case
|
|
760
788
|
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
|
761
789
|
if (member.value.callee?.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
762
790
|
inferredType = member.value.callee.name;
|
|
@@ -772,7 +800,7 @@ exports.default = (0, createRule_1.createRule)({
|
|
|
772
800
|
const fixes = [
|
|
773
801
|
fixer.insertTextAfter(propertyName, `: ${wrappedType}`),
|
|
774
802
|
];
|
|
775
|
-
//
|
|
803
|
+
// If needed, add Opt import
|
|
776
804
|
if (needsImport) {
|
|
777
805
|
const importFix = addOptImport(fixer);
|
|
778
806
|
if (importFix)
|
|
@@ -784,10 +812,10 @@ exports.default = (0, createRule_1.createRule)({
|
|
|
784
812
|
}
|
|
785
813
|
}
|
|
786
814
|
}
|
|
787
|
-
//
|
|
815
|
+
// If there is an @Enum decorator, check its configuration regardless of whether the type is recognized as enum
|
|
788
816
|
if (enumDecorator) {
|
|
789
817
|
const enumConfig = parseEnumDecorator(enumDecorator);
|
|
790
|
-
//
|
|
818
|
+
// Check if nullable configuration matches the TypeScript type
|
|
791
819
|
if (enumConfig.nullable !== typeInfo.isNullable) {
|
|
792
820
|
const expectedEnumText = buildEnumDecorator(typeInfo);
|
|
793
821
|
context.report({
|
|
@@ -798,12 +826,12 @@ exports.default = (0, createRule_1.createRule)({
|
|
|
798
826
|
},
|
|
799
827
|
});
|
|
800
828
|
}
|
|
801
|
-
//
|
|
829
|
+
// Properties with @Enum decorator do not need @Property decorator
|
|
802
830
|
return;
|
|
803
831
|
}
|
|
804
|
-
//
|
|
832
|
+
// If it's an enum type but doesn't have @Enum decorator
|
|
805
833
|
if (typeInfo.isEnum) {
|
|
806
|
-
//
|
|
834
|
+
// If it has @Property decorator, suggest replacing with @Enum
|
|
807
835
|
if (propertyDecorator) {
|
|
808
836
|
context.report({
|
|
809
837
|
node: propertyDecorator,
|
|
@@ -815,7 +843,7 @@ exports.default = (0, createRule_1.createRule)({
|
|
|
815
843
|
});
|
|
816
844
|
return;
|
|
817
845
|
}
|
|
818
|
-
//
|
|
846
|
+
// If it doesn't have @Enum decorator, add one
|
|
819
847
|
context.report({
|
|
820
848
|
node: member,
|
|
821
849
|
messageId: "useEnumDecorator",
|
|
@@ -826,7 +854,7 @@ exports.default = (0, createRule_1.createRule)({
|
|
|
826
854
|
});
|
|
827
855
|
return;
|
|
828
856
|
}
|
|
829
|
-
//
|
|
857
|
+
// Non-enum type: if no @Property decorator, add one
|
|
830
858
|
if (!propertyDecorator) {
|
|
831
859
|
context.report({
|
|
832
860
|
node: member,
|
|
@@ -838,34 +866,39 @@ exports.default = (0, createRule_1.createRule)({
|
|
|
838
866
|
});
|
|
839
867
|
return;
|
|
840
868
|
}
|
|
841
|
-
//
|
|
869
|
+
// Check if existing decorator matches the type
|
|
842
870
|
const currentConfig = parsePropertyDecorator(propertyDecorator);
|
|
843
|
-
|
|
871
|
+
let expectedType = typeInfo.propertyType;
|
|
872
|
+
// PrimaryKey number type expects t.integer
|
|
873
|
+
if (currentDecoratorName === "PrimaryKey" &&
|
|
874
|
+
expectedType === "t.float") {
|
|
875
|
+
expectedType = "t.integer";
|
|
876
|
+
}
|
|
844
877
|
let needReport = false;
|
|
845
|
-
//
|
|
878
|
+
// Check type configuration
|
|
846
879
|
if (expectedType && currentConfig.type !== expectedType) {
|
|
847
|
-
//
|
|
880
|
+
// For string type, accept multiple valid configurations
|
|
848
881
|
if (expectedType === "t.string" &&
|
|
849
882
|
isValidStringType(currentConfig.type)) {
|
|
850
|
-
//
|
|
883
|
+
// Current config is a valid string type configuration, no modification needed
|
|
851
884
|
}
|
|
852
|
-
else if (expectedType === "t.float" &&
|
|
885
|
+
else if ((expectedType === "t.float" || expectedType === "t.integer") &&
|
|
853
886
|
isValidNumberType(currentConfig.type)) {
|
|
854
|
-
//
|
|
887
|
+
// Current config is a valid number type configuration, no modification needed
|
|
855
888
|
}
|
|
856
889
|
else if (expectedType === "t.datetime" &&
|
|
857
890
|
isValidDateType(currentConfig.type)) {
|
|
858
|
-
//
|
|
891
|
+
// Current config is a valid Date type configuration, no modification needed
|
|
859
892
|
}
|
|
860
893
|
else if (expectedType === "t.array" &&
|
|
861
894
|
typeInfo.arrayElementTypeName === "number" &&
|
|
862
895
|
isValidNumberArrayType(currentConfig.type)) {
|
|
863
|
-
//
|
|
896
|
+
// Current config is a valid number[] type configuration (t.array or VectorType), no modification needed
|
|
864
897
|
}
|
|
865
898
|
else if (expectedType === "t.json") {
|
|
866
|
-
//
|
|
899
|
+
// For t.json type (Record or custom type arrays)
|
|
867
900
|
if (currentConfig.type === "t.json") {
|
|
868
|
-
// t.json
|
|
901
|
+
// t.json config is correct, no modification needed
|
|
869
902
|
}
|
|
870
903
|
else {
|
|
871
904
|
needReport = true;
|
|
@@ -876,10 +909,10 @@ exports.default = (0, createRule_1.createRule)({
|
|
|
876
909
|
}
|
|
877
910
|
}
|
|
878
911
|
else if (!expectedType && currentConfig.type) {
|
|
879
|
-
//
|
|
912
|
+
// If type is not needed but currently has one, needs correction
|
|
880
913
|
needReport = true;
|
|
881
914
|
}
|
|
882
|
-
//
|
|
915
|
+
// Check nullable configuration
|
|
883
916
|
if (currentConfig.nullable !== typeInfo.isNullable) {
|
|
884
917
|
needReport = true;
|
|
885
918
|
}
|
|
@@ -889,7 +922,7 @@ exports.default = (0, createRule_1.createRule)({
|
|
|
889
922
|
node: member,
|
|
890
923
|
messageId: "alignPropertyDecoratorWithTsType",
|
|
891
924
|
fix: (fixer) => {
|
|
892
|
-
const fixes = fixWithTypeInfo(member, propertyDecorator, typeInfo, currentConfig);
|
|
925
|
+
const fixes = fixWithTypeInfo(member, propertyDecorator, typeInfo, currentConfig, currentDecoratorName ?? "Property");
|
|
893
926
|
return applyFixes(fixer, fixes);
|
|
894
927
|
},
|
|
895
928
|
});
|