@nest-boot/eslint-plugin 7.0.4 → 7.0.6
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 +14 -0
- package/dist/rules/graphql/graphql-field-config-from-types.d.ts +3 -1
- package/dist/rules/graphql/graphql-field-config-from-types.js +39 -41
- 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 +10 -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 +106 -106
- 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 +14 -9
- package/src/rules/graphql/graphql-field-config-from-types.spec.ts +18 -18
- package/src/rules/graphql/graphql-field-config-from-types.ts +40 -44
- 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 +10 -10
- package/src/rules/mikro-orm/entity-property-config-from-types.spec.ts +22 -22
- package/src/rules/mikro-orm/entity-property-config-from-types.ts +111 -110
- package/src/utils/decorators.ts +16 -16
- package/tsconfig.json +0 -1
|
@@ -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,7 +113,7 @@ 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" ||
|
|
@@ -123,25 +124,25 @@ export default createRule<
|
|
|
123
124
|
return true;
|
|
124
125
|
}
|
|
125
126
|
|
|
126
|
-
//
|
|
127
|
+
// Accept DecimalType (class reference, without arguments)
|
|
127
128
|
if (typeConfig === "DecimalType") {
|
|
128
129
|
return true;
|
|
129
130
|
}
|
|
130
131
|
|
|
131
|
-
//
|
|
132
|
-
//
|
|
132
|
+
// Accept new DecimalType('string') and new BigIntType('string')
|
|
133
|
+
// but exclude new BigIntType('number')
|
|
133
134
|
if (
|
|
134
135
|
typeConfig.includes("DecimalType") ||
|
|
135
136
|
typeConfig.includes("BigIntType")
|
|
136
137
|
) {
|
|
137
|
-
//
|
|
138
|
+
// If it's new BigIntType('number'), it's invalid
|
|
138
139
|
if (
|
|
139
140
|
typeConfig.includes("BigIntType") &&
|
|
140
141
|
(typeConfig.includes("'number'") || typeConfig.includes('"number"'))
|
|
141
142
|
) {
|
|
142
143
|
return false;
|
|
143
144
|
}
|
|
144
|
-
//
|
|
145
|
+
// Other cases (including new XXXType('string') and DecimalType) are valid
|
|
145
146
|
return true;
|
|
146
147
|
}
|
|
147
148
|
|
|
@@ -151,7 +152,7 @@ export default createRule<
|
|
|
151
152
|
const isValidNumberType = (typeConfig: string | null): boolean => {
|
|
152
153
|
if (!typeConfig) return false;
|
|
153
154
|
|
|
154
|
-
//
|
|
155
|
+
// Accept t.integer, t.float, t.double, t.decimal
|
|
155
156
|
if (
|
|
156
157
|
typeConfig === "t.integer" ||
|
|
157
158
|
typeConfig === "t.float" ||
|
|
@@ -161,25 +162,25 @@ export default createRule<
|
|
|
161
162
|
return true;
|
|
162
163
|
}
|
|
163
164
|
|
|
164
|
-
//
|
|
165
|
+
// Accept BigIntType and DecimalType (class reference, without arguments)
|
|
165
166
|
if (typeConfig === "BigIntType" || typeConfig === "DecimalType") {
|
|
166
167
|
return true;
|
|
167
168
|
}
|
|
168
169
|
|
|
169
|
-
//
|
|
170
|
-
//
|
|
170
|
+
// Accept new DecimalType('number') and new BigIntType('number')
|
|
171
|
+
// but exclude new XXXType('string')
|
|
171
172
|
if (
|
|
172
173
|
typeConfig.includes("DecimalType") ||
|
|
173
174
|
typeConfig.includes("BigIntType")
|
|
174
175
|
) {
|
|
175
|
-
//
|
|
176
|
+
// If it contains 'string', it's invalid for number type
|
|
176
177
|
if (
|
|
177
178
|
typeConfig.includes("'string'") ||
|
|
178
179
|
typeConfig.includes('"string"')
|
|
179
180
|
) {
|
|
180
181
|
return false;
|
|
181
182
|
}
|
|
182
|
-
//
|
|
183
|
+
// Other cases (including new XXXType('number')) are valid
|
|
183
184
|
return true;
|
|
184
185
|
}
|
|
185
186
|
|
|
@@ -189,17 +190,17 @@ export default createRule<
|
|
|
189
190
|
const isValidNumberArrayType = (typeConfig: string | null): boolean => {
|
|
190
191
|
if (!typeConfig) return false;
|
|
191
192
|
|
|
192
|
-
//
|
|
193
|
+
// Accept t.array (default)
|
|
193
194
|
if (typeConfig === "t.array") {
|
|
194
195
|
return true;
|
|
195
196
|
}
|
|
196
197
|
|
|
197
|
-
//
|
|
198
|
+
// Accept VectorType (class reference)
|
|
198
199
|
if (typeConfig === "VectorType") {
|
|
199
200
|
return true;
|
|
200
201
|
}
|
|
201
202
|
|
|
202
|
-
//
|
|
203
|
+
// Accept new VectorType(...)
|
|
203
204
|
if (typeConfig.includes("VectorType")) {
|
|
204
205
|
return true;
|
|
205
206
|
}
|
|
@@ -210,7 +211,7 @@ export default createRule<
|
|
|
210
211
|
const isValidDateType = (typeConfig: string | null): boolean => {
|
|
211
212
|
if (!typeConfig) return false;
|
|
212
213
|
|
|
213
|
-
//
|
|
214
|
+
// Accept t.datetime, t.date, t.time
|
|
214
215
|
if (
|
|
215
216
|
typeConfig === "t.datetime" ||
|
|
216
217
|
typeConfig === "t.date" ||
|
|
@@ -256,7 +257,7 @@ export default createRule<
|
|
|
256
257
|
);
|
|
257
258
|
};
|
|
258
259
|
|
|
259
|
-
//
|
|
260
|
+
// Check if the type is wrapped with Opt<T>
|
|
260
261
|
const isWrappedWithOpt = (
|
|
261
262
|
property: TSESTree.PropertyDefinition,
|
|
262
263
|
): boolean => {
|
|
@@ -273,13 +274,13 @@ export default createRule<
|
|
|
273
274
|
);
|
|
274
275
|
};
|
|
275
276
|
|
|
276
|
-
//
|
|
277
|
+
// Check if Opt is imported
|
|
277
278
|
const hasOptImport = (): boolean => {
|
|
278
279
|
const program = context.sourceCode.ast;
|
|
279
280
|
for (const statement of program.body) {
|
|
280
281
|
if (statement.type === AST_NODE_TYPES.ImportDeclaration) {
|
|
281
282
|
const importSource = statement.source.value;
|
|
282
|
-
//
|
|
283
|
+
// Check imports from @mikro-orm/*
|
|
283
284
|
if (
|
|
284
285
|
typeof importSource === "string" &&
|
|
285
286
|
importSource.startsWith("@mikro-orm/")
|
|
@@ -300,11 +301,11 @@ export default createRule<
|
|
|
300
301
|
return false;
|
|
301
302
|
};
|
|
302
303
|
|
|
303
|
-
//
|
|
304
|
+
// Add Opt to the @mikro-orm/core import
|
|
304
305
|
const addOptImport = (fixer: RuleFixer): RuleFix | null => {
|
|
305
306
|
const program = context.sourceCode.ast;
|
|
306
307
|
|
|
307
|
-
//
|
|
308
|
+
// Find the @mikro-orm/core import statement
|
|
308
309
|
let coreImport: TSESTree.ImportDeclaration | null = null;
|
|
309
310
|
|
|
310
311
|
for (const statement of program.body) {
|
|
@@ -318,24 +319,24 @@ export default createRule<
|
|
|
318
319
|
}
|
|
319
320
|
|
|
320
321
|
if (coreImport) {
|
|
321
|
-
//
|
|
322
|
+
// Already has @mikro-orm/core import, add Opt to the import list
|
|
322
323
|
const lastSpecifier =
|
|
323
324
|
coreImport.specifiers[coreImport.specifiers.length - 1];
|
|
324
325
|
|
|
325
|
-
//
|
|
326
|
+
// Check if it's a multiline import
|
|
326
327
|
const importText = source.getText(coreImport);
|
|
327
328
|
const isMultiline = importText.includes("\n");
|
|
328
329
|
|
|
329
330
|
if (isMultiline) {
|
|
330
|
-
//
|
|
331
|
-
const indent = " "; //
|
|
331
|
+
// Multiline import: add after the last import item, keeping indentation
|
|
332
|
+
const indent = " "; // Assuming 2-space indentation
|
|
332
333
|
return fixer.insertTextAfter(lastSpecifier, `,\n${indent}Opt`);
|
|
333
334
|
} else {
|
|
334
|
-
//
|
|
335
|
+
// Single-line import: add directly
|
|
335
336
|
return fixer.insertTextAfter(lastSpecifier, ", Opt");
|
|
336
337
|
}
|
|
337
338
|
} else {
|
|
338
|
-
//
|
|
339
|
+
// No @mikro-orm/core import, add a new import statement at the top
|
|
339
340
|
const firstImport = program.body.find(
|
|
340
341
|
(node: TSESTree.ProgramStatement) =>
|
|
341
342
|
node.type === AST_NODE_TYPES.ImportDeclaration,
|
|
@@ -363,12 +364,12 @@ export default createRule<
|
|
|
363
364
|
? property.typeAnnotation.typeAnnotation
|
|
364
365
|
: null;
|
|
365
366
|
|
|
366
|
-
//
|
|
367
|
+
// Optional property (?) is treated as nullable
|
|
367
368
|
if (property.optional) {
|
|
368
369
|
isNullable = true;
|
|
369
370
|
}
|
|
370
371
|
|
|
371
|
-
//
|
|
372
|
+
// Handle null/undefined in union types
|
|
372
373
|
if (baseTypeNode?.type === AST_NODE_TYPES.TSUnionType) {
|
|
373
374
|
const hasNullish = baseTypeNode.types.some((t: TSESTree.TypeNode) => {
|
|
374
375
|
return (
|
|
@@ -387,7 +388,7 @@ export default createRule<
|
|
|
387
388
|
}) ?? null;
|
|
388
389
|
}
|
|
389
390
|
|
|
390
|
-
//
|
|
391
|
+
// First unwrap Ref<T> and Opt<T> → T, and handle null/undefined within
|
|
391
392
|
if (
|
|
392
393
|
baseTypeNode?.type === AST_NODE_TYPES.TSTypeReference &&
|
|
393
394
|
baseTypeNode.typeName.type === AST_NODE_TYPES.Identifier &&
|
|
@@ -395,7 +396,7 @@ export default createRule<
|
|
|
395
396
|
baseTypeNode.typeName.name === "Opt")
|
|
396
397
|
) {
|
|
397
398
|
let inner = baseTypeNode.typeArguments?.params[0] ?? null;
|
|
398
|
-
if (inner
|
|
399
|
+
if (inner?.type === AST_NODE_TYPES.TSUnionType) {
|
|
399
400
|
const hasNullish = inner.types.some((t: TSESTree.TypeNode) => {
|
|
400
401
|
return (
|
|
401
402
|
t.type === AST_NODE_TYPES.TSNullKeyword ||
|
|
@@ -414,12 +415,12 @@ export default createRule<
|
|
|
414
415
|
baseTypeNode = inner ?? baseTypeNode;
|
|
415
416
|
}
|
|
416
417
|
|
|
417
|
-
//
|
|
418
|
+
// Skip Collection<T> types (these should be handled by OneToMany etc. decorators)
|
|
418
419
|
if (baseTypeNode && isCollectionType(baseTypeNode)) {
|
|
419
420
|
return null;
|
|
420
421
|
}
|
|
421
422
|
|
|
422
|
-
//
|
|
423
|
+
// Array type (T[] or Array<T>) - checked after unwrapping Opt/Ref
|
|
423
424
|
const elementTypeNode = baseTypeNode
|
|
424
425
|
? extractArrayElementType(baseTypeNode)
|
|
425
426
|
: null;
|
|
@@ -430,12 +431,12 @@ export default createRule<
|
|
|
430
431
|
const targetTypeNode: TSESTree.TypeNode | null =
|
|
431
432
|
elementTypeNode ?? baseTypeNode;
|
|
432
433
|
|
|
433
|
-
//
|
|
434
|
+
// Check if target type is an enum
|
|
434
435
|
if (targetTypeNode) {
|
|
435
436
|
isEnum = isEnumType(targetTypeNode);
|
|
436
437
|
}
|
|
437
438
|
|
|
438
|
-
//
|
|
439
|
+
// When no explicit type, try to infer from literal initializer
|
|
439
440
|
if (!targetTypeNode) {
|
|
440
441
|
if (property.value?.type === AST_NODE_TYPES.Literal) {
|
|
441
442
|
const value = property.value.value;
|
|
@@ -487,7 +488,7 @@ export default createRule<
|
|
|
487
488
|
};
|
|
488
489
|
}
|
|
489
490
|
|
|
490
|
-
//
|
|
491
|
+
// Keyword types
|
|
491
492
|
if (targetTypeNode.type === AST_NODE_TYPES.TSStringKeyword) {
|
|
492
493
|
return {
|
|
493
494
|
typeName: "string",
|
|
@@ -519,10 +520,10 @@ export default createRule<
|
|
|
519
520
|
};
|
|
520
521
|
}
|
|
521
522
|
|
|
522
|
-
//
|
|
523
|
+
// Identifier (class/custom type)
|
|
523
524
|
const ident = getIdentifierName(targetTypeNode);
|
|
524
525
|
if (ident) {
|
|
525
|
-
//
|
|
526
|
+
// For arrays, custom type arrays use t.json
|
|
526
527
|
if (isArray) {
|
|
527
528
|
return {
|
|
528
529
|
typeName: ident,
|
|
@@ -547,7 +548,7 @@ export default createRule<
|
|
|
547
548
|
return null;
|
|
548
549
|
};
|
|
549
550
|
|
|
550
|
-
//
|
|
551
|
+
// Wrap type with Opt<T>
|
|
551
552
|
const wrapWithOpt = (typeString: string): string => {
|
|
552
553
|
return `Opt<${typeString}>`;
|
|
553
554
|
};
|
|
@@ -577,12 +578,12 @@ export default createRule<
|
|
|
577
578
|
): string => {
|
|
578
579
|
const options: string[] = [];
|
|
579
580
|
|
|
580
|
-
//
|
|
581
|
+
// If there is a propertyType configuration, add type
|
|
581
582
|
if (info.propertyType) {
|
|
582
583
|
options.push(`type: ${info.propertyType}`);
|
|
583
584
|
}
|
|
584
585
|
|
|
585
|
-
//
|
|
586
|
+
// Add other properties (keeping original order)
|
|
586
587
|
for (const prop of otherProps) {
|
|
587
588
|
options.push(`${prop.key}: ${prop.value}`);
|
|
588
589
|
}
|
|
@@ -628,10 +629,10 @@ export default createRule<
|
|
|
628
629
|
) => {
|
|
629
630
|
const fixes: CustomFix[] = [];
|
|
630
631
|
|
|
631
|
-
//
|
|
632
|
+
// If current config already has a valid value, keep it instead of replacing with default
|
|
632
633
|
const finalInfo = { ...info };
|
|
633
634
|
|
|
634
|
-
// PrimaryKey
|
|
635
|
+
// PrimaryKey number type defaults to t.integer
|
|
635
636
|
if (
|
|
636
637
|
decoratorName === "PrimaryKey" &&
|
|
637
638
|
finalInfo.propertyType === "t.float"
|
|
@@ -643,31 +644,31 @@ export default createRule<
|
|
|
643
644
|
info.propertyType === "t.string" &&
|
|
644
645
|
isValidStringType(currentConfig.type)
|
|
645
646
|
) {
|
|
646
|
-
//
|
|
647
|
+
// Keep valid string type config
|
|
647
648
|
finalInfo.propertyType = currentConfig.type;
|
|
648
649
|
} else if (
|
|
649
650
|
(info.propertyType === "t.float" ||
|
|
650
651
|
info.propertyType === "t.integer") &&
|
|
651
652
|
isValidNumberType(currentConfig.type)
|
|
652
653
|
) {
|
|
653
|
-
//
|
|
654
|
+
// Keep valid number type config
|
|
654
655
|
finalInfo.propertyType = currentConfig.type;
|
|
655
656
|
} else if (
|
|
656
657
|
info.propertyType === "t.datetime" &&
|
|
657
658
|
isValidDateType(currentConfig.type)
|
|
658
659
|
) {
|
|
659
|
-
//
|
|
660
|
+
// Keep valid Date type config
|
|
660
661
|
finalInfo.propertyType = currentConfig.type;
|
|
661
662
|
} else if (
|
|
662
663
|
info.isArray &&
|
|
663
664
|
info.arrayElementTypeName === "number" &&
|
|
664
665
|
isValidNumberArrayType(currentConfig.type)
|
|
665
666
|
) {
|
|
666
|
-
//
|
|
667
|
+
// Keep valid number[] type config (VectorType)
|
|
667
668
|
finalInfo.propertyType = currentConfig.type;
|
|
668
669
|
}
|
|
669
670
|
|
|
670
|
-
//
|
|
671
|
+
// Keep other property configs
|
|
671
672
|
const newDecoratorText = buildPropertyDecorator(
|
|
672
673
|
finalInfo,
|
|
673
674
|
currentConfig.otherProps,
|
|
@@ -736,7 +737,7 @@ export default createRule<
|
|
|
736
737
|
nullable = true;
|
|
737
738
|
}
|
|
738
739
|
} else {
|
|
739
|
-
//
|
|
740
|
+
// Keep all other properties
|
|
740
741
|
otherProps.push({
|
|
741
742
|
key: prop.key.name,
|
|
742
743
|
value: source.getText(prop.value),
|
|
@@ -789,23 +790,23 @@ export default createRule<
|
|
|
789
790
|
return { items, nullable };
|
|
790
791
|
};
|
|
791
792
|
|
|
792
|
-
//
|
|
793
|
+
// Check if the file uses the Opt type but hasn't imported it
|
|
793
794
|
const checkOptUsageWithoutImport = (node: TSESTree.ClassDeclaration) => {
|
|
794
795
|
let usesOpt = false;
|
|
795
796
|
|
|
796
|
-
//
|
|
797
|
+
// Iterate through all members to check for Opt<T> type annotations
|
|
797
798
|
node.body.body.forEach((member: TSESTree.ClassElement) => {
|
|
798
799
|
if (member.type !== AST_NODE_TYPES.PropertyDefinition) return;
|
|
799
800
|
|
|
800
801
|
const typeAnnotation = member.typeAnnotation?.typeAnnotation;
|
|
801
802
|
if (!typeAnnotation) return;
|
|
802
803
|
|
|
803
|
-
//
|
|
804
|
+
// Recursively check if the type node contains Opt
|
|
804
805
|
const containsOpt = (typeNode: TSESTree.TypeNode): boolean => {
|
|
805
806
|
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
|
806
807
|
if (!typeNode) return false;
|
|
807
808
|
|
|
808
|
-
//
|
|
809
|
+
// Check if it's Opt<T>
|
|
809
810
|
if (
|
|
810
811
|
typeNode.type === AST_NODE_TYPES.TSTypeReference &&
|
|
811
812
|
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
|
@@ -815,14 +816,14 @@ export default createRule<
|
|
|
815
816
|
return true;
|
|
816
817
|
}
|
|
817
818
|
|
|
818
|
-
//
|
|
819
|
+
// Recursively check union types
|
|
819
820
|
if (typeNode.type === AST_NODE_TYPES.TSUnionType) {
|
|
820
821
|
return typeNode.types.some((t: TSESTree.TypeNode) =>
|
|
821
822
|
containsOpt(t),
|
|
822
823
|
);
|
|
823
824
|
}
|
|
824
825
|
|
|
825
|
-
//
|
|
826
|
+
// Recursively check type parameters
|
|
826
827
|
if (
|
|
827
828
|
typeNode.type === AST_NODE_TYPES.TSTypeReference &&
|
|
828
829
|
typeNode.typeArguments?.params
|
|
@@ -832,7 +833,7 @@ export default createRule<
|
|
|
832
833
|
);
|
|
833
834
|
}
|
|
834
835
|
|
|
835
|
-
//
|
|
836
|
+
// Recursively check array element type
|
|
836
837
|
if (typeNode.type === AST_NODE_TYPES.TSArrayType) {
|
|
837
838
|
return containsOpt(typeNode.elementType);
|
|
838
839
|
}
|
|
@@ -845,7 +846,7 @@ export default createRule<
|
|
|
845
846
|
}
|
|
846
847
|
});
|
|
847
848
|
|
|
848
|
-
//
|
|
849
|
+
// If Opt is used but not imported, report an error
|
|
849
850
|
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
|
850
851
|
if (usesOpt && !hasOptImport()) {
|
|
851
852
|
context.report({
|
|
@@ -863,13 +864,13 @@ export default createRule<
|
|
|
863
864
|
ClassDeclaration(node) {
|
|
864
865
|
if (!isEntityClass(node)) return;
|
|
865
866
|
|
|
866
|
-
//
|
|
867
|
+
// First check if Opt is used but not imported
|
|
867
868
|
checkOptUsageWithoutImport(node);
|
|
868
869
|
|
|
869
870
|
node.body.body.forEach((member: TSESTree.ClassElement) => {
|
|
870
871
|
if (member.type !== AST_NODE_TYPES.PropertyDefinition) return;
|
|
871
872
|
|
|
872
|
-
//
|
|
873
|
+
// Check relation decorators (OneToOne, OneToMany, ManyToOne, ManyToMany)
|
|
873
874
|
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
|
874
875
|
const hasRelationDecorator = member.decorators?.some(
|
|
875
876
|
(decorator: TSESTree.Decorator) => {
|
|
@@ -889,10 +890,10 @@ export default createRule<
|
|
|
889
890
|
},
|
|
890
891
|
);
|
|
891
892
|
|
|
892
|
-
//
|
|
893
|
+
// If there are relation decorators, skip checking (these properties don't need @Property)
|
|
893
894
|
if (hasRelationDecorator) return;
|
|
894
895
|
|
|
895
|
-
//
|
|
896
|
+
// Property-like decorators (decorators that need type checking)
|
|
896
897
|
const propertyLikeDecorators = [
|
|
897
898
|
"Property",
|
|
898
899
|
"PrimaryKey",
|
|
@@ -900,7 +901,7 @@ export default createRule<
|
|
|
900
901
|
"HashedProperty",
|
|
901
902
|
];
|
|
902
903
|
|
|
903
|
-
//
|
|
904
|
+
// Find property-like decorator
|
|
904
905
|
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
|
905
906
|
const propertyLikeDecorator = member.decorators?.find(
|
|
906
907
|
(decorator: TSESTree.Decorator) => {
|
|
@@ -916,7 +917,7 @@ export default createRule<
|
|
|
916
917
|
},
|
|
917
918
|
);
|
|
918
919
|
|
|
919
|
-
//
|
|
920
|
+
// Get decorator name
|
|
920
921
|
const getDecoratorName = (
|
|
921
922
|
decorator: TSESTree.Decorator,
|
|
922
923
|
): string | null => {
|
|
@@ -929,7 +930,7 @@ export default createRule<
|
|
|
929
930
|
return null;
|
|
930
931
|
};
|
|
931
932
|
|
|
932
|
-
//
|
|
933
|
+
// Check @Enum decorator
|
|
933
934
|
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
|
934
935
|
const enumDecorator = member.decorators?.find(
|
|
935
936
|
(decorator: TSESTree.Decorator) => {
|
|
@@ -942,7 +943,7 @@ export default createRule<
|
|
|
942
943
|
},
|
|
943
944
|
);
|
|
944
945
|
|
|
945
|
-
//
|
|
946
|
+
// Use property-like decorator
|
|
946
947
|
const propertyDecorator = propertyLikeDecorator;
|
|
947
948
|
const currentDecoratorName = propertyDecorator
|
|
948
949
|
? getDecoratorName(propertyDecorator)
|
|
@@ -952,11 +953,11 @@ export default createRule<
|
|
|
952
953
|
|
|
953
954
|
if (!typeInfo?.typeName) return;
|
|
954
955
|
|
|
955
|
-
//
|
|
956
|
+
// Check initializer and Opt<T> type match
|
|
956
957
|
const hasInitializer = member.value !== null;
|
|
957
958
|
const isOptWrapped = isWrappedWithOpt(member);
|
|
958
959
|
|
|
959
|
-
//
|
|
960
|
+
// Check if the initializer is null
|
|
960
961
|
|
|
961
962
|
const isInitializedToNull =
|
|
962
963
|
hasInitializer &&
|
|
@@ -964,14 +965,14 @@ export default createRule<
|
|
|
964
965
|
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
|
965
966
|
member.value?.value === null;
|
|
966
967
|
|
|
967
|
-
//
|
|
968
|
+
// Case 1: Has non-null initializer but not wrapped with Opt<T>
|
|
968
969
|
if (
|
|
969
970
|
hasInitializer &&
|
|
970
971
|
!isInitializedToNull &&
|
|
971
972
|
!isOptWrapped &&
|
|
972
973
|
!member.optional
|
|
973
974
|
) {
|
|
974
|
-
//
|
|
975
|
+
// Has initializer but not wrapped with Opt<T>, needs to report error
|
|
975
976
|
const typeAnnotation = member.typeAnnotation?.typeAnnotation;
|
|
976
977
|
const needsImport = !hasOptImport();
|
|
977
978
|
|
|
@@ -987,7 +988,7 @@ export default createRule<
|
|
|
987
988
|
fixer.replaceText(typeAnnotation, wrappedType),
|
|
988
989
|
];
|
|
989
990
|
|
|
990
|
-
//
|
|
991
|
+
// If needed, add Opt import
|
|
991
992
|
if (needsImport) {
|
|
992
993
|
const importFix = addOptImport(fixer);
|
|
993
994
|
if (importFix) fixes.push(importFix);
|
|
@@ -997,7 +998,7 @@ export default createRule<
|
|
|
997
998
|
},
|
|
998
999
|
});
|
|
999
1000
|
} else if (member.value) {
|
|
1000
|
-
//
|
|
1001
|
+
// No type annotation, infer type from initializer
|
|
1001
1002
|
let inferredType: string | null = null;
|
|
1002
1003
|
|
|
1003
1004
|
if (member.value.type === AST_NODE_TYPES.Literal) {
|
|
@@ -1006,10 +1007,10 @@ export default createRule<
|
|
|
1006
1007
|
else if (valueType === "number") inferredType = "number";
|
|
1007
1008
|
else if (valueType === "string") inferredType = "string";
|
|
1008
1009
|
} else if (member.value.type === AST_NODE_TYPES.ArrayExpression) {
|
|
1009
|
-
//
|
|
1010
|
+
// Empty array [] case, need to infer type from @Property decorator
|
|
1010
1011
|
inferredType = "unknown[]";
|
|
1011
1012
|
} else if (member.value.type === AST_NODE_TYPES.NewExpression) {
|
|
1012
|
-
// new Date()
|
|
1013
|
+
// new Date() case
|
|
1013
1014
|
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
|
1014
1015
|
if (member.value.callee?.type === AST_NODE_TYPES.Identifier) {
|
|
1015
1016
|
inferredType = member.value.callee.name;
|
|
@@ -1028,7 +1029,7 @@ export default createRule<
|
|
|
1028
1029
|
fixer.insertTextAfter(propertyName, `: ${wrappedType}`),
|
|
1029
1030
|
];
|
|
1030
1031
|
|
|
1031
|
-
//
|
|
1032
|
+
// If needed, add Opt import
|
|
1032
1033
|
if (needsImport) {
|
|
1033
1034
|
const importFix = addOptImport(fixer);
|
|
1034
1035
|
if (importFix) fixes.push(importFix);
|
|
@@ -1041,11 +1042,11 @@ export default createRule<
|
|
|
1041
1042
|
}
|
|
1042
1043
|
}
|
|
1043
1044
|
|
|
1044
|
-
//
|
|
1045
|
+
// If there is an @Enum decorator, check its configuration regardless of whether the type is recognized as enum
|
|
1045
1046
|
if (enumDecorator) {
|
|
1046
1047
|
const enumConfig = parseEnumDecorator(enumDecorator);
|
|
1047
1048
|
|
|
1048
|
-
//
|
|
1049
|
+
// Check if nullable configuration matches the TypeScript type
|
|
1049
1050
|
if (enumConfig.nullable !== typeInfo.isNullable) {
|
|
1050
1051
|
const expectedEnumText = buildEnumDecorator(typeInfo);
|
|
1051
1052
|
context.report({
|
|
@@ -1059,13 +1060,13 @@ export default createRule<
|
|
|
1059
1060
|
},
|
|
1060
1061
|
});
|
|
1061
1062
|
}
|
|
1062
|
-
//
|
|
1063
|
+
// Properties with @Enum decorator do not need @Property decorator
|
|
1063
1064
|
return;
|
|
1064
1065
|
}
|
|
1065
1066
|
|
|
1066
|
-
//
|
|
1067
|
+
// If it's an enum type but doesn't have @Enum decorator
|
|
1067
1068
|
if (typeInfo.isEnum) {
|
|
1068
|
-
//
|
|
1069
|
+
// If it has @Property decorator, suggest replacing with @Enum
|
|
1069
1070
|
if (propertyDecorator) {
|
|
1070
1071
|
context.report({
|
|
1071
1072
|
node: propertyDecorator,
|
|
@@ -1081,7 +1082,7 @@ export default createRule<
|
|
|
1081
1082
|
return;
|
|
1082
1083
|
}
|
|
1083
1084
|
|
|
1084
|
-
//
|
|
1085
|
+
// If it doesn't have @Enum decorator, add one
|
|
1085
1086
|
context.report({
|
|
1086
1087
|
node: member,
|
|
1087
1088
|
messageId: "useEnumDecorator",
|
|
@@ -1096,7 +1097,7 @@ export default createRule<
|
|
|
1096
1097
|
return;
|
|
1097
1098
|
}
|
|
1098
1099
|
|
|
1099
|
-
//
|
|
1100
|
+
// Non-enum type: if no @Property decorator, add one
|
|
1100
1101
|
if (!propertyDecorator) {
|
|
1101
1102
|
context.report({
|
|
1102
1103
|
node: member,
|
|
@@ -1109,11 +1110,11 @@ export default createRule<
|
|
|
1109
1110
|
return;
|
|
1110
1111
|
}
|
|
1111
1112
|
|
|
1112
|
-
//
|
|
1113
|
+
// Check if existing decorator matches the type
|
|
1113
1114
|
const currentConfig = parsePropertyDecorator(propertyDecorator);
|
|
1114
1115
|
let expectedType = typeInfo.propertyType;
|
|
1115
1116
|
|
|
1116
|
-
// PrimaryKey
|
|
1117
|
+
// PrimaryKey number type expects t.integer
|
|
1117
1118
|
if (
|
|
1118
1119
|
currentDecoratorName === "PrimaryKey" &&
|
|
1119
1120
|
expectedType === "t.float"
|
|
@@ -1123,34 +1124,34 @@ export default createRule<
|
|
|
1123
1124
|
|
|
1124
1125
|
let needReport = false;
|
|
1125
1126
|
|
|
1126
|
-
//
|
|
1127
|
+
// Check type configuration
|
|
1127
1128
|
if (expectedType && currentConfig.type !== expectedType) {
|
|
1128
|
-
//
|
|
1129
|
+
// For string type, accept multiple valid configurations
|
|
1129
1130
|
if (
|
|
1130
1131
|
expectedType === "t.string" &&
|
|
1131
1132
|
isValidStringType(currentConfig.type)
|
|
1132
1133
|
) {
|
|
1133
|
-
//
|
|
1134
|
+
// Current config is a valid string type configuration, no modification needed
|
|
1134
1135
|
} else if (
|
|
1135
1136
|
(expectedType === "t.float" || expectedType === "t.integer") &&
|
|
1136
1137
|
isValidNumberType(currentConfig.type)
|
|
1137
1138
|
) {
|
|
1138
|
-
//
|
|
1139
|
+
// Current config is a valid number type configuration, no modification needed
|
|
1139
1140
|
} else if (
|
|
1140
1141
|
expectedType === "t.datetime" &&
|
|
1141
1142
|
isValidDateType(currentConfig.type)
|
|
1142
1143
|
) {
|
|
1143
|
-
//
|
|
1144
|
+
// Current config is a valid Date type configuration, no modification needed
|
|
1144
1145
|
} else if (
|
|
1145
1146
|
expectedType === "t.array" &&
|
|
1146
1147
|
typeInfo.arrayElementTypeName === "number" &&
|
|
1147
1148
|
isValidNumberArrayType(currentConfig.type)
|
|
1148
1149
|
) {
|
|
1149
|
-
//
|
|
1150
|
+
// Current config is a valid number[] type configuration (t.array or VectorType), no modification needed
|
|
1150
1151
|
} else if (expectedType === "t.json") {
|
|
1151
|
-
//
|
|
1152
|
+
// For t.json type (Record or custom type arrays)
|
|
1152
1153
|
if (currentConfig.type === "t.json") {
|
|
1153
|
-
// t.json
|
|
1154
|
+
// t.json config is correct, no modification needed
|
|
1154
1155
|
} else {
|
|
1155
1156
|
needReport = true;
|
|
1156
1157
|
}
|
|
@@ -1158,11 +1159,11 @@ export default createRule<
|
|
|
1158
1159
|
needReport = true;
|
|
1159
1160
|
}
|
|
1160
1161
|
} else if (!expectedType && currentConfig.type) {
|
|
1161
|
-
//
|
|
1162
|
+
// If type is not needed but currently has one, needs correction
|
|
1162
1163
|
needReport = true;
|
|
1163
1164
|
}
|
|
1164
1165
|
|
|
1165
|
-
//
|
|
1166
|
+
// Check nullable configuration
|
|
1166
1167
|
if (currentConfig.nullable !== typeInfo.isNullable) {
|
|
1167
1168
|
needReport = true;
|
|
1168
1169
|
}
|