@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
|
@@ -9,7 +9,7 @@ import * as ts from "typescript";
|
|
|
9
9
|
import { createRule } from "../../utils/createRule";
|
|
10
10
|
import { hasClassDecorator } from "../../utils/decorators";
|
|
11
11
|
|
|
12
|
-
//
|
|
12
|
+
// Custom Fix object type for deferred fix application
|
|
13
13
|
interface CustomFix {
|
|
14
14
|
type: "insert" | "replace";
|
|
15
15
|
range: readonly [number, number];
|
|
@@ -20,9 +20,9 @@ interface TypeInfo {
|
|
|
20
20
|
typeName: string | null;
|
|
21
21
|
isArray: boolean;
|
|
22
22
|
isNullable: boolean;
|
|
23
|
-
isEnum: boolean; //
|
|
24
|
-
propertyType: string | null; // MikroORM
|
|
25
|
-
arrayElementTypeName?: string | null; //
|
|
23
|
+
isEnum: boolean; // Whether the type is an enum
|
|
24
|
+
propertyType: string | null; // MikroORM type such as t.text, t.bigint, etc.
|
|
25
|
+
arrayElementTypeName?: string | null; // Type name of array elements (for special validation)
|
|
26
26
|
}
|
|
27
27
|
|
|
28
28
|
export default createRule<
|
|
@@ -38,20 +38,21 @@ export default createRule<
|
|
|
38
38
|
type: "problem",
|
|
39
39
|
docs: {
|
|
40
40
|
description:
|
|
41
|
-
"
|
|
41
|
+
"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.",
|
|
42
42
|
},
|
|
43
43
|
fixable: "code",
|
|
44
44
|
schema: [],
|
|
45
45
|
messages: {
|
|
46
46
|
alignPropertyDecoratorWithTsType:
|
|
47
|
-
"@Property
|
|
47
|
+
"@Property decorator should align with the TypeScript type (type and nullable).",
|
|
48
48
|
removePropertyDecorator:
|
|
49
|
-
"
|
|
50
|
-
useEnumDecorator:
|
|
49
|
+
"Property has a @{{decoratorName}} decorator, @Property decorator should be removed.",
|
|
50
|
+
useEnumDecorator:
|
|
51
|
+
"Enum types should use @Enum decorator instead of @Property decorator.",
|
|
51
52
|
useOptTypeForInitializedProperty:
|
|
52
|
-
"
|
|
53
|
+
"Properties with non-null initializers should use the Opt<T> type wrapper.",
|
|
53
54
|
removeOptTypeForNonInitializedProperty:
|
|
54
|
-
"
|
|
55
|
+
"Properties without initializers or initialized to null should not use the Opt<T> type wrapper.",
|
|
55
56
|
},
|
|
56
57
|
},
|
|
57
58
|
defaultOptions: [],
|
|
@@ -60,18 +61,18 @@ export default createRule<
|
|
|
60
61
|
const parserServices = ESLintUtils.getParserServices(context);
|
|
61
62
|
const checker = parserServices.program.getTypeChecker();
|
|
62
63
|
|
|
63
|
-
//
|
|
64
|
+
// Check if the type is an enum
|
|
64
65
|
const isEnumType = (node: TSESTree.Node): boolean => {
|
|
65
66
|
try {
|
|
66
67
|
const tsNode = parserServices.esTreeNodeToTSNodeMap.get(node);
|
|
67
68
|
const type = checker.getTypeAtLocation(tsNode);
|
|
68
69
|
|
|
69
|
-
//
|
|
70
|
+
// Check if it is an enum type
|
|
70
71
|
if (type.symbol.flags & ts.SymbolFlags.Enum) {
|
|
71
72
|
return true;
|
|
72
73
|
}
|
|
73
74
|
|
|
74
|
-
//
|
|
75
|
+
// Check each member in a union type
|
|
75
76
|
if (type.isUnion()) {
|
|
76
77
|
return type.types.some(
|
|
77
78
|
(t) =>
|
|
@@ -92,15 +93,15 @@ export default createRule<
|
|
|
92
93
|
switch (typeName) {
|
|
93
94
|
case "String":
|
|
94
95
|
case "string":
|
|
95
|
-
return "t.string"; //
|
|
96
|
+
return "t.string"; // Default to t.string
|
|
96
97
|
case "Number":
|
|
97
98
|
case "number":
|
|
98
|
-
return "t.float"; //
|
|
99
|
+
return "t.float"; // Default to t.float
|
|
99
100
|
case "Boolean":
|
|
100
101
|
case "boolean":
|
|
101
|
-
return "t.boolean"; //
|
|
102
|
+
return "t.boolean"; // Default to t.boolean
|
|
102
103
|
case "Date":
|
|
103
|
-
return "t.datetime"; //
|
|
104
|
+
return "t.datetime"; // Default to t.datetime
|
|
104
105
|
case "GraphQLJSONObject":
|
|
105
106
|
case "Record":
|
|
106
107
|
return "t.json";
|
|
@@ -112,35 +113,36 @@ export default createRule<
|
|
|
112
113
|
const isValidStringType = (typeConfig: string | null): boolean => {
|
|
113
114
|
if (!typeConfig) return false;
|
|
114
115
|
|
|
115
|
-
//
|
|
116
|
+
// Accept t.string, t.text, t.uuid, t.decimal, t.bigint
|
|
116
117
|
if (
|
|
117
118
|
typeConfig === "t.string" ||
|
|
118
119
|
typeConfig === "t.text" ||
|
|
120
|
+
typeConfig === "t.uuid" ||
|
|
119
121
|
typeConfig === "t.decimal" ||
|
|
120
122
|
typeConfig === "t.bigint"
|
|
121
123
|
) {
|
|
122
124
|
return true;
|
|
123
125
|
}
|
|
124
126
|
|
|
125
|
-
//
|
|
127
|
+
// Accept DecimalType (class reference, without arguments)
|
|
126
128
|
if (typeConfig === "DecimalType") {
|
|
127
129
|
return true;
|
|
128
130
|
}
|
|
129
131
|
|
|
130
|
-
//
|
|
131
|
-
//
|
|
132
|
+
// Accept new DecimalType('string') and new BigIntType('string')
|
|
133
|
+
// but exclude new BigIntType('number')
|
|
132
134
|
if (
|
|
133
135
|
typeConfig.includes("DecimalType") ||
|
|
134
136
|
typeConfig.includes("BigIntType")
|
|
135
137
|
) {
|
|
136
|
-
//
|
|
138
|
+
// If it's new BigIntType('number'), it's invalid
|
|
137
139
|
if (
|
|
138
140
|
typeConfig.includes("BigIntType") &&
|
|
139
141
|
(typeConfig.includes("'number'") || typeConfig.includes('"number"'))
|
|
140
142
|
) {
|
|
141
143
|
return false;
|
|
142
144
|
}
|
|
143
|
-
//
|
|
145
|
+
// Other cases (including new XXXType('string') and DecimalType) are valid
|
|
144
146
|
return true;
|
|
145
147
|
}
|
|
146
148
|
|
|
@@ -150,7 +152,7 @@ export default createRule<
|
|
|
150
152
|
const isValidNumberType = (typeConfig: string | null): boolean => {
|
|
151
153
|
if (!typeConfig) return false;
|
|
152
154
|
|
|
153
|
-
//
|
|
155
|
+
// Accept t.integer, t.float, t.double, t.decimal
|
|
154
156
|
if (
|
|
155
157
|
typeConfig === "t.integer" ||
|
|
156
158
|
typeConfig === "t.float" ||
|
|
@@ -160,25 +162,25 @@ export default createRule<
|
|
|
160
162
|
return true;
|
|
161
163
|
}
|
|
162
164
|
|
|
163
|
-
//
|
|
165
|
+
// Accept BigIntType and DecimalType (class reference, without arguments)
|
|
164
166
|
if (typeConfig === "BigIntType" || typeConfig === "DecimalType") {
|
|
165
167
|
return true;
|
|
166
168
|
}
|
|
167
169
|
|
|
168
|
-
//
|
|
169
|
-
//
|
|
170
|
+
// Accept new DecimalType('number') and new BigIntType('number')
|
|
171
|
+
// but exclude new XXXType('string')
|
|
170
172
|
if (
|
|
171
173
|
typeConfig.includes("DecimalType") ||
|
|
172
174
|
typeConfig.includes("BigIntType")
|
|
173
175
|
) {
|
|
174
|
-
//
|
|
176
|
+
// If it contains 'string', it's invalid for number type
|
|
175
177
|
if (
|
|
176
178
|
typeConfig.includes("'string'") ||
|
|
177
179
|
typeConfig.includes('"string"')
|
|
178
180
|
) {
|
|
179
181
|
return false;
|
|
180
182
|
}
|
|
181
|
-
//
|
|
183
|
+
// Other cases (including new XXXType('number')) are valid
|
|
182
184
|
return true;
|
|
183
185
|
}
|
|
184
186
|
|
|
@@ -188,17 +190,17 @@ export default createRule<
|
|
|
188
190
|
const isValidNumberArrayType = (typeConfig: string | null): boolean => {
|
|
189
191
|
if (!typeConfig) return false;
|
|
190
192
|
|
|
191
|
-
//
|
|
193
|
+
// Accept t.array (default)
|
|
192
194
|
if (typeConfig === "t.array") {
|
|
193
195
|
return true;
|
|
194
196
|
}
|
|
195
197
|
|
|
196
|
-
//
|
|
198
|
+
// Accept VectorType (class reference)
|
|
197
199
|
if (typeConfig === "VectorType") {
|
|
198
200
|
return true;
|
|
199
201
|
}
|
|
200
202
|
|
|
201
|
-
//
|
|
203
|
+
// Accept new VectorType(...)
|
|
202
204
|
if (typeConfig.includes("VectorType")) {
|
|
203
205
|
return true;
|
|
204
206
|
}
|
|
@@ -209,7 +211,7 @@ export default createRule<
|
|
|
209
211
|
const isValidDateType = (typeConfig: string | null): boolean => {
|
|
210
212
|
if (!typeConfig) return false;
|
|
211
213
|
|
|
212
|
-
//
|
|
214
|
+
// Accept t.datetime, t.date, t.time
|
|
213
215
|
if (
|
|
214
216
|
typeConfig === "t.datetime" ||
|
|
215
217
|
typeConfig === "t.date" ||
|
|
@@ -255,7 +257,7 @@ export default createRule<
|
|
|
255
257
|
);
|
|
256
258
|
};
|
|
257
259
|
|
|
258
|
-
//
|
|
260
|
+
// Check if the type is wrapped with Opt<T>
|
|
259
261
|
const isWrappedWithOpt = (
|
|
260
262
|
property: TSESTree.PropertyDefinition,
|
|
261
263
|
): boolean => {
|
|
@@ -272,13 +274,13 @@ export default createRule<
|
|
|
272
274
|
);
|
|
273
275
|
};
|
|
274
276
|
|
|
275
|
-
//
|
|
277
|
+
// Check if Opt is imported
|
|
276
278
|
const hasOptImport = (): boolean => {
|
|
277
279
|
const program = context.sourceCode.ast;
|
|
278
280
|
for (const statement of program.body) {
|
|
279
281
|
if (statement.type === AST_NODE_TYPES.ImportDeclaration) {
|
|
280
282
|
const importSource = statement.source.value;
|
|
281
|
-
//
|
|
283
|
+
// Check imports from @mikro-orm/*
|
|
282
284
|
if (
|
|
283
285
|
typeof importSource === "string" &&
|
|
284
286
|
importSource.startsWith("@mikro-orm/")
|
|
@@ -299,11 +301,11 @@ export default createRule<
|
|
|
299
301
|
return false;
|
|
300
302
|
};
|
|
301
303
|
|
|
302
|
-
//
|
|
304
|
+
// Add Opt to the @mikro-orm/core import
|
|
303
305
|
const addOptImport = (fixer: RuleFixer): RuleFix | null => {
|
|
304
306
|
const program = context.sourceCode.ast;
|
|
305
307
|
|
|
306
|
-
//
|
|
308
|
+
// Find the @mikro-orm/core import statement
|
|
307
309
|
let coreImport: TSESTree.ImportDeclaration | null = null;
|
|
308
310
|
|
|
309
311
|
for (const statement of program.body) {
|
|
@@ -317,24 +319,24 @@ export default createRule<
|
|
|
317
319
|
}
|
|
318
320
|
|
|
319
321
|
if (coreImport) {
|
|
320
|
-
//
|
|
322
|
+
// Already has @mikro-orm/core import, add Opt to the import list
|
|
321
323
|
const lastSpecifier =
|
|
322
324
|
coreImport.specifiers[coreImport.specifiers.length - 1];
|
|
323
325
|
|
|
324
|
-
//
|
|
326
|
+
// Check if it's a multiline import
|
|
325
327
|
const importText = source.getText(coreImport);
|
|
326
328
|
const isMultiline = importText.includes("\n");
|
|
327
329
|
|
|
328
330
|
if (isMultiline) {
|
|
329
|
-
//
|
|
330
|
-
const indent = " "; //
|
|
331
|
+
// Multiline import: add after the last import item, keeping indentation
|
|
332
|
+
const indent = " "; // Assuming 2-space indentation
|
|
331
333
|
return fixer.insertTextAfter(lastSpecifier, `,\n${indent}Opt`);
|
|
332
334
|
} else {
|
|
333
|
-
//
|
|
335
|
+
// Single-line import: add directly
|
|
334
336
|
return fixer.insertTextAfter(lastSpecifier, ", Opt");
|
|
335
337
|
}
|
|
336
338
|
} else {
|
|
337
|
-
//
|
|
339
|
+
// No @mikro-orm/core import, add a new import statement at the top
|
|
338
340
|
const firstImport = program.body.find(
|
|
339
341
|
(node: TSESTree.ProgramStatement) =>
|
|
340
342
|
node.type === AST_NODE_TYPES.ImportDeclaration,
|
|
@@ -362,12 +364,12 @@ export default createRule<
|
|
|
362
364
|
? property.typeAnnotation.typeAnnotation
|
|
363
365
|
: null;
|
|
364
366
|
|
|
365
|
-
//
|
|
367
|
+
// Optional property (?) is treated as nullable
|
|
366
368
|
if (property.optional) {
|
|
367
369
|
isNullable = true;
|
|
368
370
|
}
|
|
369
371
|
|
|
370
|
-
//
|
|
372
|
+
// Handle null/undefined in union types
|
|
371
373
|
if (baseTypeNode?.type === AST_NODE_TYPES.TSUnionType) {
|
|
372
374
|
const hasNullish = baseTypeNode.types.some((t: TSESTree.TypeNode) => {
|
|
373
375
|
return (
|
|
@@ -386,7 +388,7 @@ export default createRule<
|
|
|
386
388
|
}) ?? null;
|
|
387
389
|
}
|
|
388
390
|
|
|
389
|
-
//
|
|
391
|
+
// First unwrap Ref<T> and Opt<T> → T, and handle null/undefined within
|
|
390
392
|
if (
|
|
391
393
|
baseTypeNode?.type === AST_NODE_TYPES.TSTypeReference &&
|
|
392
394
|
baseTypeNode.typeName.type === AST_NODE_TYPES.Identifier &&
|
|
@@ -413,12 +415,12 @@ export default createRule<
|
|
|
413
415
|
baseTypeNode = inner ?? baseTypeNode;
|
|
414
416
|
}
|
|
415
417
|
|
|
416
|
-
//
|
|
418
|
+
// Skip Collection<T> types (these should be handled by OneToMany etc. decorators)
|
|
417
419
|
if (baseTypeNode && isCollectionType(baseTypeNode)) {
|
|
418
420
|
return null;
|
|
419
421
|
}
|
|
420
422
|
|
|
421
|
-
//
|
|
423
|
+
// Array type (T[] or Array<T>) - checked after unwrapping Opt/Ref
|
|
422
424
|
const elementTypeNode = baseTypeNode
|
|
423
425
|
? extractArrayElementType(baseTypeNode)
|
|
424
426
|
: null;
|
|
@@ -429,12 +431,12 @@ export default createRule<
|
|
|
429
431
|
const targetTypeNode: TSESTree.TypeNode | null =
|
|
430
432
|
elementTypeNode ?? baseTypeNode;
|
|
431
433
|
|
|
432
|
-
//
|
|
434
|
+
// Check if target type is an enum
|
|
433
435
|
if (targetTypeNode) {
|
|
434
436
|
isEnum = isEnumType(targetTypeNode);
|
|
435
437
|
}
|
|
436
438
|
|
|
437
|
-
//
|
|
439
|
+
// When no explicit type, try to infer from literal initializer
|
|
438
440
|
if (!targetTypeNode) {
|
|
439
441
|
if (property.value?.type === AST_NODE_TYPES.Literal) {
|
|
440
442
|
const value = property.value.value;
|
|
@@ -486,7 +488,7 @@ export default createRule<
|
|
|
486
488
|
};
|
|
487
489
|
}
|
|
488
490
|
|
|
489
|
-
//
|
|
491
|
+
// Keyword types
|
|
490
492
|
if (targetTypeNode.type === AST_NODE_TYPES.TSStringKeyword) {
|
|
491
493
|
return {
|
|
492
494
|
typeName: "string",
|
|
@@ -518,10 +520,10 @@ export default createRule<
|
|
|
518
520
|
};
|
|
519
521
|
}
|
|
520
522
|
|
|
521
|
-
//
|
|
523
|
+
// Identifier (class/custom type)
|
|
522
524
|
const ident = getIdentifierName(targetTypeNode);
|
|
523
525
|
if (ident) {
|
|
524
|
-
//
|
|
526
|
+
// For arrays, custom type arrays use t.json
|
|
525
527
|
if (isArray) {
|
|
526
528
|
return {
|
|
527
529
|
typeName: ident,
|
|
@@ -546,7 +548,7 @@ export default createRule<
|
|
|
546
548
|
return null;
|
|
547
549
|
};
|
|
548
550
|
|
|
549
|
-
//
|
|
551
|
+
// Wrap type with Opt<T>
|
|
550
552
|
const wrapWithOpt = (typeString: string): string => {
|
|
551
553
|
return `Opt<${typeString}>`;
|
|
552
554
|
};
|
|
@@ -572,15 +574,16 @@ export default createRule<
|
|
|
572
574
|
const buildPropertyDecorator = (
|
|
573
575
|
info: TypeInfo,
|
|
574
576
|
otherProps: { key: string; value: string }[] = [],
|
|
577
|
+
decoratorName = "Property",
|
|
575
578
|
): string => {
|
|
576
579
|
const options: string[] = [];
|
|
577
580
|
|
|
578
|
-
//
|
|
581
|
+
// If there is a propertyType configuration, add type
|
|
579
582
|
if (info.propertyType) {
|
|
580
583
|
options.push(`type: ${info.propertyType}`);
|
|
581
584
|
}
|
|
582
585
|
|
|
583
|
-
//
|
|
586
|
+
// Add other properties (keeping original order)
|
|
584
587
|
for (const prop of otherProps) {
|
|
585
588
|
options.push(`${prop.key}: ${prop.value}`);
|
|
586
589
|
}
|
|
@@ -590,10 +593,10 @@ export default createRule<
|
|
|
590
593
|
}
|
|
591
594
|
|
|
592
595
|
if (options.length === 0) {
|
|
593
|
-
return
|
|
596
|
+
return `@${decoratorName}()`;
|
|
594
597
|
}
|
|
595
598
|
|
|
596
|
-
return
|
|
599
|
+
return `@${decoratorName}({ ${options.join(", ")} })`;
|
|
597
600
|
};
|
|
598
601
|
|
|
599
602
|
const addPropertyDecorator = (
|
|
@@ -622,42 +625,54 @@ export default createRule<
|
|
|
622
625
|
nullable: boolean;
|
|
623
626
|
otherProps: { key: string; value: string }[];
|
|
624
627
|
},
|
|
628
|
+
decoratorName = "Property",
|
|
625
629
|
) => {
|
|
626
630
|
const fixes: CustomFix[] = [];
|
|
627
631
|
|
|
628
|
-
//
|
|
632
|
+
// If current config already has a valid value, keep it instead of replacing with default
|
|
629
633
|
const finalInfo = { ...info };
|
|
634
|
+
|
|
635
|
+
// PrimaryKey number type defaults to t.integer
|
|
636
|
+
if (
|
|
637
|
+
decoratorName === "PrimaryKey" &&
|
|
638
|
+
finalInfo.propertyType === "t.float"
|
|
639
|
+
) {
|
|
640
|
+
finalInfo.propertyType = "t.integer";
|
|
641
|
+
}
|
|
642
|
+
|
|
630
643
|
if (
|
|
631
644
|
info.propertyType === "t.string" &&
|
|
632
645
|
isValidStringType(currentConfig.type)
|
|
633
646
|
) {
|
|
634
|
-
//
|
|
647
|
+
// Keep valid string type config
|
|
635
648
|
finalInfo.propertyType = currentConfig.type;
|
|
636
649
|
} else if (
|
|
637
|
-
info.propertyType === "t.float"
|
|
650
|
+
(info.propertyType === "t.float" ||
|
|
651
|
+
info.propertyType === "t.integer") &&
|
|
638
652
|
isValidNumberType(currentConfig.type)
|
|
639
653
|
) {
|
|
640
|
-
//
|
|
654
|
+
// Keep valid number type config
|
|
641
655
|
finalInfo.propertyType = currentConfig.type;
|
|
642
656
|
} else if (
|
|
643
657
|
info.propertyType === "t.datetime" &&
|
|
644
658
|
isValidDateType(currentConfig.type)
|
|
645
659
|
) {
|
|
646
|
-
//
|
|
660
|
+
// Keep valid Date type config
|
|
647
661
|
finalInfo.propertyType = currentConfig.type;
|
|
648
662
|
} else if (
|
|
649
663
|
info.isArray &&
|
|
650
664
|
info.arrayElementTypeName === "number" &&
|
|
651
665
|
isValidNumberArrayType(currentConfig.type)
|
|
652
666
|
) {
|
|
653
|
-
//
|
|
667
|
+
// Keep valid number[] type config (VectorType)
|
|
654
668
|
finalInfo.propertyType = currentConfig.type;
|
|
655
669
|
}
|
|
656
670
|
|
|
657
|
-
//
|
|
671
|
+
// Keep other property configs
|
|
658
672
|
const newDecoratorText = buildPropertyDecorator(
|
|
659
673
|
finalInfo,
|
|
660
674
|
currentConfig.otherProps,
|
|
675
|
+
decoratorName,
|
|
661
676
|
);
|
|
662
677
|
|
|
663
678
|
fixes.push({
|
|
@@ -722,7 +737,7 @@ export default createRule<
|
|
|
722
737
|
nullable = true;
|
|
723
738
|
}
|
|
724
739
|
} else {
|
|
725
|
-
//
|
|
740
|
+
// Keep all other properties
|
|
726
741
|
otherProps.push({
|
|
727
742
|
key: prop.key.name,
|
|
728
743
|
value: source.getText(prop.value),
|
|
@@ -775,23 +790,23 @@ export default createRule<
|
|
|
775
790
|
return { items, nullable };
|
|
776
791
|
};
|
|
777
792
|
|
|
778
|
-
//
|
|
793
|
+
// Check if the file uses the Opt type but hasn't imported it
|
|
779
794
|
const checkOptUsageWithoutImport = (node: TSESTree.ClassDeclaration) => {
|
|
780
795
|
let usesOpt = false;
|
|
781
796
|
|
|
782
|
-
//
|
|
797
|
+
// Iterate through all members to check for Opt<T> type annotations
|
|
783
798
|
node.body.body.forEach((member: TSESTree.ClassElement) => {
|
|
784
799
|
if (member.type !== AST_NODE_TYPES.PropertyDefinition) return;
|
|
785
800
|
|
|
786
801
|
const typeAnnotation = member.typeAnnotation?.typeAnnotation;
|
|
787
802
|
if (!typeAnnotation) return;
|
|
788
803
|
|
|
789
|
-
//
|
|
804
|
+
// Recursively check if the type node contains Opt
|
|
790
805
|
const containsOpt = (typeNode: TSESTree.TypeNode): boolean => {
|
|
791
806
|
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
|
792
807
|
if (!typeNode) return false;
|
|
793
808
|
|
|
794
|
-
//
|
|
809
|
+
// Check if it's Opt<T>
|
|
795
810
|
if (
|
|
796
811
|
typeNode.type === AST_NODE_TYPES.TSTypeReference &&
|
|
797
812
|
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
|
@@ -801,14 +816,14 @@ export default createRule<
|
|
|
801
816
|
return true;
|
|
802
817
|
}
|
|
803
818
|
|
|
804
|
-
//
|
|
819
|
+
// Recursively check union types
|
|
805
820
|
if (typeNode.type === AST_NODE_TYPES.TSUnionType) {
|
|
806
821
|
return typeNode.types.some((t: TSESTree.TypeNode) =>
|
|
807
822
|
containsOpt(t),
|
|
808
823
|
);
|
|
809
824
|
}
|
|
810
825
|
|
|
811
|
-
//
|
|
826
|
+
// Recursively check type parameters
|
|
812
827
|
if (
|
|
813
828
|
typeNode.type === AST_NODE_TYPES.TSTypeReference &&
|
|
814
829
|
typeNode.typeArguments?.params
|
|
@@ -818,7 +833,7 @@ export default createRule<
|
|
|
818
833
|
);
|
|
819
834
|
}
|
|
820
835
|
|
|
821
|
-
//
|
|
836
|
+
// Recursively check array element type
|
|
822
837
|
if (typeNode.type === AST_NODE_TYPES.TSArrayType) {
|
|
823
838
|
return containsOpt(typeNode.elementType);
|
|
824
839
|
}
|
|
@@ -831,7 +846,7 @@ export default createRule<
|
|
|
831
846
|
}
|
|
832
847
|
});
|
|
833
848
|
|
|
834
|
-
//
|
|
849
|
+
// If Opt is used but not imported, report an error
|
|
835
850
|
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
|
836
851
|
if (usesOpt && !hasOptImport()) {
|
|
837
852
|
context.report({
|
|
@@ -849,13 +864,13 @@ export default createRule<
|
|
|
849
864
|
ClassDeclaration(node) {
|
|
850
865
|
if (!isEntityClass(node)) return;
|
|
851
866
|
|
|
852
|
-
//
|
|
867
|
+
// First check if Opt is used but not imported
|
|
853
868
|
checkOptUsageWithoutImport(node);
|
|
854
869
|
|
|
855
870
|
node.body.body.forEach((member: TSESTree.ClassElement) => {
|
|
856
871
|
if (member.type !== AST_NODE_TYPES.PropertyDefinition) return;
|
|
857
872
|
|
|
858
|
-
//
|
|
873
|
+
// Check relation decorators (OneToOne, OneToMany, ManyToOne, ManyToMany)
|
|
859
874
|
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
|
860
875
|
const hasRelationDecorator = member.decorators?.some(
|
|
861
876
|
(decorator: TSESTree.Decorator) => {
|
|
@@ -865,7 +880,6 @@ export default createRule<
|
|
|
865
880
|
) {
|
|
866
881
|
const decoratorName = decorator.expression.callee.name;
|
|
867
882
|
return [
|
|
868
|
-
"PrimaryKey",
|
|
869
883
|
"OneToOne",
|
|
870
884
|
"OneToMany",
|
|
871
885
|
"ManyToOne",
|
|
@@ -876,43 +890,74 @@ export default createRule<
|
|
|
876
890
|
},
|
|
877
891
|
);
|
|
878
892
|
|
|
879
|
-
//
|
|
893
|
+
// If there are relation decorators, skip checking (these properties don't need @Property)
|
|
880
894
|
if (hasRelationDecorator) return;
|
|
881
895
|
|
|
882
|
-
//
|
|
896
|
+
// Property-like decorators (decorators that need type checking)
|
|
897
|
+
const propertyLikeDecorators = [
|
|
898
|
+
"Property",
|
|
899
|
+
"PrimaryKey",
|
|
900
|
+
"EncryptedProperty",
|
|
901
|
+
"HashedProperty",
|
|
902
|
+
];
|
|
903
|
+
|
|
904
|
+
// Find property-like decorator
|
|
883
905
|
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
|
884
|
-
const
|
|
906
|
+
const propertyLikeDecorator = member.decorators?.find(
|
|
885
907
|
(decorator: TSESTree.Decorator) => {
|
|
886
|
-
|
|
908
|
+
if (
|
|
887
909
|
decorator.expression.type === AST_NODE_TYPES.CallExpression &&
|
|
888
|
-
decorator.expression.callee.type ===
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
910
|
+
decorator.expression.callee.type === AST_NODE_TYPES.Identifier
|
|
911
|
+
) {
|
|
912
|
+
return propertyLikeDecorators.includes(
|
|
913
|
+
decorator.expression.callee.name,
|
|
914
|
+
);
|
|
915
|
+
}
|
|
916
|
+
return false;
|
|
892
917
|
},
|
|
893
918
|
);
|
|
894
919
|
|
|
920
|
+
// Get decorator name
|
|
921
|
+
const getDecoratorName = (
|
|
922
|
+
decorator: TSESTree.Decorator,
|
|
923
|
+
): string | null => {
|
|
924
|
+
if (
|
|
925
|
+
decorator.expression.type === AST_NODE_TYPES.CallExpression &&
|
|
926
|
+
decorator.expression.callee.type === AST_NODE_TYPES.Identifier
|
|
927
|
+
) {
|
|
928
|
+
return decorator.expression.callee.name;
|
|
929
|
+
}
|
|
930
|
+
return null;
|
|
931
|
+
};
|
|
932
|
+
|
|
933
|
+
// Check @Enum decorator
|
|
895
934
|
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
|
896
|
-
const
|
|
935
|
+
const enumDecorator = member.decorators?.find(
|
|
897
936
|
(decorator: TSESTree.Decorator) => {
|
|
898
937
|
return (
|
|
899
938
|
decorator.expression.type === AST_NODE_TYPES.CallExpression &&
|
|
900
939
|
decorator.expression.callee.type ===
|
|
901
940
|
AST_NODE_TYPES.Identifier &&
|
|
902
|
-
decorator.expression.callee.name === "
|
|
941
|
+
decorator.expression.callee.name === "Enum"
|
|
903
942
|
);
|
|
904
943
|
},
|
|
905
944
|
);
|
|
906
945
|
|
|
946
|
+
// Use property-like decorator
|
|
947
|
+
const propertyDecorator = propertyLikeDecorator;
|
|
948
|
+
const currentDecoratorName = propertyDecorator
|
|
949
|
+
? getDecoratorName(propertyDecorator)
|
|
950
|
+
: null;
|
|
951
|
+
|
|
907
952
|
const typeInfo = computeTypeInfo(member);
|
|
908
953
|
|
|
909
954
|
if (!typeInfo?.typeName) return;
|
|
910
955
|
|
|
911
|
-
//
|
|
956
|
+
// Check initializer and Opt<T> type match
|
|
912
957
|
const hasInitializer = member.value !== null;
|
|
913
958
|
const isOptWrapped = isWrappedWithOpt(member);
|
|
914
959
|
|
|
915
|
-
//
|
|
960
|
+
// Check if the initializer is null
|
|
916
961
|
|
|
917
962
|
const isInitializedToNull =
|
|
918
963
|
hasInitializer &&
|
|
@@ -920,14 +965,14 @@ export default createRule<
|
|
|
920
965
|
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
|
921
966
|
member.value?.value === null;
|
|
922
967
|
|
|
923
|
-
//
|
|
968
|
+
// Case 1: Has non-null initializer but not wrapped with Opt<T>
|
|
924
969
|
if (
|
|
925
970
|
hasInitializer &&
|
|
926
971
|
!isInitializedToNull &&
|
|
927
972
|
!isOptWrapped &&
|
|
928
973
|
!member.optional
|
|
929
974
|
) {
|
|
930
|
-
//
|
|
975
|
+
// Has initializer but not wrapped with Opt<T>, needs to report error
|
|
931
976
|
const typeAnnotation = member.typeAnnotation?.typeAnnotation;
|
|
932
977
|
const needsImport = !hasOptImport();
|
|
933
978
|
|
|
@@ -943,7 +988,7 @@ export default createRule<
|
|
|
943
988
|
fixer.replaceText(typeAnnotation, wrappedType),
|
|
944
989
|
];
|
|
945
990
|
|
|
946
|
-
//
|
|
991
|
+
// If needed, add Opt import
|
|
947
992
|
if (needsImport) {
|
|
948
993
|
const importFix = addOptImport(fixer);
|
|
949
994
|
if (importFix) fixes.push(importFix);
|
|
@@ -953,7 +998,7 @@ export default createRule<
|
|
|
953
998
|
},
|
|
954
999
|
});
|
|
955
1000
|
} else if (member.value) {
|
|
956
|
-
//
|
|
1001
|
+
// No type annotation, infer type from initializer
|
|
957
1002
|
let inferredType: string | null = null;
|
|
958
1003
|
|
|
959
1004
|
if (member.value.type === AST_NODE_TYPES.Literal) {
|
|
@@ -962,10 +1007,10 @@ export default createRule<
|
|
|
962
1007
|
else if (valueType === "number") inferredType = "number";
|
|
963
1008
|
else if (valueType === "string") inferredType = "string";
|
|
964
1009
|
} else if (member.value.type === AST_NODE_TYPES.ArrayExpression) {
|
|
965
|
-
//
|
|
1010
|
+
// Empty array [] case, need to infer type from @Property decorator
|
|
966
1011
|
inferredType = "unknown[]";
|
|
967
1012
|
} else if (member.value.type === AST_NODE_TYPES.NewExpression) {
|
|
968
|
-
// new Date()
|
|
1013
|
+
// new Date() case
|
|
969
1014
|
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
|
970
1015
|
if (member.value.callee?.type === AST_NODE_TYPES.Identifier) {
|
|
971
1016
|
inferredType = member.value.callee.name;
|
|
@@ -984,7 +1029,7 @@ export default createRule<
|
|
|
984
1029
|
fixer.insertTextAfter(propertyName, `: ${wrappedType}`),
|
|
985
1030
|
];
|
|
986
1031
|
|
|
987
|
-
//
|
|
1032
|
+
// If needed, add Opt import
|
|
988
1033
|
if (needsImport) {
|
|
989
1034
|
const importFix = addOptImport(fixer);
|
|
990
1035
|
if (importFix) fixes.push(importFix);
|
|
@@ -997,11 +1042,11 @@ export default createRule<
|
|
|
997
1042
|
}
|
|
998
1043
|
}
|
|
999
1044
|
|
|
1000
|
-
//
|
|
1045
|
+
// If there is an @Enum decorator, check its configuration regardless of whether the type is recognized as enum
|
|
1001
1046
|
if (enumDecorator) {
|
|
1002
1047
|
const enumConfig = parseEnumDecorator(enumDecorator);
|
|
1003
1048
|
|
|
1004
|
-
//
|
|
1049
|
+
// Check if nullable configuration matches the TypeScript type
|
|
1005
1050
|
if (enumConfig.nullable !== typeInfo.isNullable) {
|
|
1006
1051
|
const expectedEnumText = buildEnumDecorator(typeInfo);
|
|
1007
1052
|
context.report({
|
|
@@ -1015,13 +1060,13 @@ export default createRule<
|
|
|
1015
1060
|
},
|
|
1016
1061
|
});
|
|
1017
1062
|
}
|
|
1018
|
-
//
|
|
1063
|
+
// Properties with @Enum decorator do not need @Property decorator
|
|
1019
1064
|
return;
|
|
1020
1065
|
}
|
|
1021
1066
|
|
|
1022
|
-
//
|
|
1067
|
+
// If it's an enum type but doesn't have @Enum decorator
|
|
1023
1068
|
if (typeInfo.isEnum) {
|
|
1024
|
-
//
|
|
1069
|
+
// If it has @Property decorator, suggest replacing with @Enum
|
|
1025
1070
|
if (propertyDecorator) {
|
|
1026
1071
|
context.report({
|
|
1027
1072
|
node: propertyDecorator,
|
|
@@ -1037,7 +1082,7 @@ export default createRule<
|
|
|
1037
1082
|
return;
|
|
1038
1083
|
}
|
|
1039
1084
|
|
|
1040
|
-
//
|
|
1085
|
+
// If it doesn't have @Enum decorator, add one
|
|
1041
1086
|
context.report({
|
|
1042
1087
|
node: member,
|
|
1043
1088
|
messageId: "useEnumDecorator",
|
|
@@ -1052,7 +1097,7 @@ export default createRule<
|
|
|
1052
1097
|
return;
|
|
1053
1098
|
}
|
|
1054
1099
|
|
|
1055
|
-
//
|
|
1100
|
+
// Non-enum type: if no @Property decorator, add one
|
|
1056
1101
|
if (!propertyDecorator) {
|
|
1057
1102
|
context.report({
|
|
1058
1103
|
node: member,
|
|
@@ -1065,40 +1110,48 @@ export default createRule<
|
|
|
1065
1110
|
return;
|
|
1066
1111
|
}
|
|
1067
1112
|
|
|
1068
|
-
//
|
|
1113
|
+
// Check if existing decorator matches the type
|
|
1069
1114
|
const currentConfig = parsePropertyDecorator(propertyDecorator);
|
|
1070
|
-
|
|
1115
|
+
let expectedType = typeInfo.propertyType;
|
|
1116
|
+
|
|
1117
|
+
// PrimaryKey number type expects t.integer
|
|
1118
|
+
if (
|
|
1119
|
+
currentDecoratorName === "PrimaryKey" &&
|
|
1120
|
+
expectedType === "t.float"
|
|
1121
|
+
) {
|
|
1122
|
+
expectedType = "t.integer";
|
|
1123
|
+
}
|
|
1071
1124
|
|
|
1072
1125
|
let needReport = false;
|
|
1073
1126
|
|
|
1074
|
-
//
|
|
1127
|
+
// Check type configuration
|
|
1075
1128
|
if (expectedType && currentConfig.type !== expectedType) {
|
|
1076
|
-
//
|
|
1129
|
+
// For string type, accept multiple valid configurations
|
|
1077
1130
|
if (
|
|
1078
1131
|
expectedType === "t.string" &&
|
|
1079
1132
|
isValidStringType(currentConfig.type)
|
|
1080
1133
|
) {
|
|
1081
|
-
//
|
|
1134
|
+
// Current config is a valid string type configuration, no modification needed
|
|
1082
1135
|
} else if (
|
|
1083
|
-
expectedType === "t.float" &&
|
|
1136
|
+
(expectedType === "t.float" || expectedType === "t.integer") &&
|
|
1084
1137
|
isValidNumberType(currentConfig.type)
|
|
1085
1138
|
) {
|
|
1086
|
-
//
|
|
1139
|
+
// Current config is a valid number type configuration, no modification needed
|
|
1087
1140
|
} else if (
|
|
1088
1141
|
expectedType === "t.datetime" &&
|
|
1089
1142
|
isValidDateType(currentConfig.type)
|
|
1090
1143
|
) {
|
|
1091
|
-
//
|
|
1144
|
+
// Current config is a valid Date type configuration, no modification needed
|
|
1092
1145
|
} else if (
|
|
1093
1146
|
expectedType === "t.array" &&
|
|
1094
1147
|
typeInfo.arrayElementTypeName === "number" &&
|
|
1095
1148
|
isValidNumberArrayType(currentConfig.type)
|
|
1096
1149
|
) {
|
|
1097
|
-
//
|
|
1150
|
+
// Current config is a valid number[] type configuration (t.array or VectorType), no modification needed
|
|
1098
1151
|
} else if (expectedType === "t.json") {
|
|
1099
|
-
//
|
|
1152
|
+
// For t.json type (Record or custom type arrays)
|
|
1100
1153
|
if (currentConfig.type === "t.json") {
|
|
1101
|
-
// t.json
|
|
1154
|
+
// t.json config is correct, no modification needed
|
|
1102
1155
|
} else {
|
|
1103
1156
|
needReport = true;
|
|
1104
1157
|
}
|
|
@@ -1106,11 +1159,11 @@ export default createRule<
|
|
|
1106
1159
|
needReport = true;
|
|
1107
1160
|
}
|
|
1108
1161
|
} else if (!expectedType && currentConfig.type) {
|
|
1109
|
-
//
|
|
1162
|
+
// If type is not needed but currently has one, needs correction
|
|
1110
1163
|
needReport = true;
|
|
1111
1164
|
}
|
|
1112
1165
|
|
|
1113
|
-
//
|
|
1166
|
+
// Check nullable configuration
|
|
1114
1167
|
if (currentConfig.nullable !== typeInfo.isNullable) {
|
|
1115
1168
|
needReport = true;
|
|
1116
1169
|
}
|
|
@@ -1126,6 +1179,7 @@ export default createRule<
|
|
|
1126
1179
|
propertyDecorator,
|
|
1127
1180
|
typeInfo,
|
|
1128
1181
|
currentConfig,
|
|
1182
|
+
currentDecoratorName ?? "Property",
|
|
1129
1183
|
);
|
|
1130
1184
|
return applyFixes(fixer, fixes);
|
|
1131
1185
|
},
|