@nest-boot/eslint-plugin 7.0.4 → 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 +6 -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 +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 +105 -105
- 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 +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 +110 -109
- 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,7 +104,7 @@ 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
110
|
typeConfig === "t.uuid" ||
|
|
@@ -112,20 +112,20 @@ exports.default = (0, createRule_1.createRule)({
|
|
|
112
112
|
typeConfig === "t.bigint") {
|
|
113
113
|
return true;
|
|
114
114
|
}
|
|
115
|
-
//
|
|
115
|
+
// Accept DecimalType (class reference, without arguments)
|
|
116
116
|
if (typeConfig === "DecimalType") {
|
|
117
117
|
return true;
|
|
118
118
|
}
|
|
119
|
-
//
|
|
120
|
-
//
|
|
119
|
+
// Accept new DecimalType('string') and new BigIntType('string')
|
|
120
|
+
// but exclude new BigIntType('number')
|
|
121
121
|
if (typeConfig.includes("DecimalType") ||
|
|
122
122
|
typeConfig.includes("BigIntType")) {
|
|
123
|
-
//
|
|
123
|
+
// If it's new BigIntType('number'), it's invalid
|
|
124
124
|
if (typeConfig.includes("BigIntType") &&
|
|
125
125
|
(typeConfig.includes("'number'") || typeConfig.includes('"number"'))) {
|
|
126
126
|
return false;
|
|
127
127
|
}
|
|
128
|
-
//
|
|
128
|
+
// Other cases (including new XXXType('string') and DecimalType) are valid
|
|
129
129
|
return true;
|
|
130
130
|
}
|
|
131
131
|
return false;
|
|
@@ -133,27 +133,27 @@ exports.default = (0, createRule_1.createRule)({
|
|
|
133
133
|
const isValidNumberType = (typeConfig) => {
|
|
134
134
|
if (!typeConfig)
|
|
135
135
|
return false;
|
|
136
|
-
//
|
|
136
|
+
// Accept t.integer, t.float, t.double, t.decimal
|
|
137
137
|
if (typeConfig === "t.integer" ||
|
|
138
138
|
typeConfig === "t.float" ||
|
|
139
139
|
typeConfig === "t.double" ||
|
|
140
140
|
typeConfig === "t.decimal") {
|
|
141
141
|
return true;
|
|
142
142
|
}
|
|
143
|
-
//
|
|
143
|
+
// Accept BigIntType and DecimalType (class reference, without arguments)
|
|
144
144
|
if (typeConfig === "BigIntType" || typeConfig === "DecimalType") {
|
|
145
145
|
return true;
|
|
146
146
|
}
|
|
147
|
-
//
|
|
148
|
-
//
|
|
147
|
+
// Accept new DecimalType('number') and new BigIntType('number')
|
|
148
|
+
// but exclude new XXXType('string')
|
|
149
149
|
if (typeConfig.includes("DecimalType") ||
|
|
150
150
|
typeConfig.includes("BigIntType")) {
|
|
151
|
-
//
|
|
151
|
+
// If it contains 'string', it's invalid for number type
|
|
152
152
|
if (typeConfig.includes("'string'") ||
|
|
153
153
|
typeConfig.includes('"string"')) {
|
|
154
154
|
return false;
|
|
155
155
|
}
|
|
156
|
-
//
|
|
156
|
+
// Other cases (including new XXXType('number')) are valid
|
|
157
157
|
return true;
|
|
158
158
|
}
|
|
159
159
|
return false;
|
|
@@ -161,15 +161,15 @@ exports.default = (0, createRule_1.createRule)({
|
|
|
161
161
|
const isValidNumberArrayType = (typeConfig) => {
|
|
162
162
|
if (!typeConfig)
|
|
163
163
|
return false;
|
|
164
|
-
//
|
|
164
|
+
// Accept t.array (default)
|
|
165
165
|
if (typeConfig === "t.array") {
|
|
166
166
|
return true;
|
|
167
167
|
}
|
|
168
|
-
//
|
|
168
|
+
// Accept VectorType (class reference)
|
|
169
169
|
if (typeConfig === "VectorType") {
|
|
170
170
|
return true;
|
|
171
171
|
}
|
|
172
|
-
//
|
|
172
|
+
// Accept new VectorType(...)
|
|
173
173
|
if (typeConfig.includes("VectorType")) {
|
|
174
174
|
return true;
|
|
175
175
|
}
|
|
@@ -178,7 +178,7 @@ exports.default = (0, createRule_1.createRule)({
|
|
|
178
178
|
const isValidDateType = (typeConfig) => {
|
|
179
179
|
if (!typeConfig)
|
|
180
180
|
return false;
|
|
181
|
-
//
|
|
181
|
+
// Accept t.datetime, t.date, t.time
|
|
182
182
|
if (typeConfig === "t.datetime" ||
|
|
183
183
|
typeConfig === "t.date" ||
|
|
184
184
|
typeConfig === "t.time") {
|
|
@@ -209,7 +209,7 @@ exports.default = (0, createRule_1.createRule)({
|
|
|
209
209
|
node.typeName.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
210
210
|
node.typeName.name === "Collection");
|
|
211
211
|
};
|
|
212
|
-
//
|
|
212
|
+
// Check if the type is wrapped with Opt<T>
|
|
213
213
|
const isWrappedWithOpt = (property) => {
|
|
214
214
|
const typeAnnotation = property.typeAnnotation;
|
|
215
215
|
if (typeAnnotation?.type !== utils_1.AST_NODE_TYPES.TSTypeAnnotation) {
|
|
@@ -220,13 +220,13 @@ exports.default = (0, createRule_1.createRule)({
|
|
|
220
220
|
typeNode.typeName.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
221
221
|
typeNode.typeName.name === "Opt");
|
|
222
222
|
};
|
|
223
|
-
//
|
|
223
|
+
// Check if Opt is imported
|
|
224
224
|
const hasOptImport = () => {
|
|
225
225
|
const program = context.sourceCode.ast;
|
|
226
226
|
for (const statement of program.body) {
|
|
227
227
|
if (statement.type === utils_1.AST_NODE_TYPES.ImportDeclaration) {
|
|
228
228
|
const importSource = statement.source.value;
|
|
229
|
-
//
|
|
229
|
+
// Check imports from @mikro-orm/*
|
|
230
230
|
if (typeof importSource === "string" &&
|
|
231
231
|
importSource.startsWith("@mikro-orm/")) {
|
|
232
232
|
const hasOpt = statement.specifiers.some((spec) => {
|
|
@@ -241,10 +241,10 @@ exports.default = (0, createRule_1.createRule)({
|
|
|
241
241
|
}
|
|
242
242
|
return false;
|
|
243
243
|
};
|
|
244
|
-
//
|
|
244
|
+
// Add Opt to the @mikro-orm/core import
|
|
245
245
|
const addOptImport = (fixer) => {
|
|
246
246
|
const program = context.sourceCode.ast;
|
|
247
|
-
//
|
|
247
|
+
// Find the @mikro-orm/core import statement
|
|
248
248
|
let coreImport = null;
|
|
249
249
|
for (const statement of program.body) {
|
|
250
250
|
if (statement.type === utils_1.AST_NODE_TYPES.ImportDeclaration) {
|
|
@@ -256,23 +256,23 @@ exports.default = (0, createRule_1.createRule)({
|
|
|
256
256
|
}
|
|
257
257
|
}
|
|
258
258
|
if (coreImport) {
|
|
259
|
-
//
|
|
259
|
+
// Already has @mikro-orm/core import, add Opt to the import list
|
|
260
260
|
const lastSpecifier = coreImport.specifiers[coreImport.specifiers.length - 1];
|
|
261
|
-
//
|
|
261
|
+
// Check if it's a multiline import
|
|
262
262
|
const importText = source.getText(coreImport);
|
|
263
263
|
const isMultiline = importText.includes("\n");
|
|
264
264
|
if (isMultiline) {
|
|
265
|
-
//
|
|
266
|
-
const indent = " "; //
|
|
265
|
+
// Multiline import: add after the last import item, keeping indentation
|
|
266
|
+
const indent = " "; // Assuming 2-space indentation
|
|
267
267
|
return fixer.insertTextAfter(lastSpecifier, `,\n${indent}Opt`);
|
|
268
268
|
}
|
|
269
269
|
else {
|
|
270
|
-
//
|
|
270
|
+
// Single-line import: add directly
|
|
271
271
|
return fixer.insertTextAfter(lastSpecifier, ", Opt");
|
|
272
272
|
}
|
|
273
273
|
}
|
|
274
274
|
else {
|
|
275
|
-
//
|
|
275
|
+
// No @mikro-orm/core import, add a new import statement at the top
|
|
276
276
|
const firstImport = program.body.find((node) => node.type === utils_1.AST_NODE_TYPES.ImportDeclaration);
|
|
277
277
|
if (firstImport) {
|
|
278
278
|
return fixer.insertTextBefore(firstImport, "import { Opt } from '@mikro-orm/core';\n");
|
|
@@ -287,11 +287,11 @@ exports.default = (0, createRule_1.createRule)({
|
|
|
287
287
|
let baseTypeNode = property.typeAnnotation?.type === utils_1.AST_NODE_TYPES.TSTypeAnnotation
|
|
288
288
|
? property.typeAnnotation.typeAnnotation
|
|
289
289
|
: null;
|
|
290
|
-
//
|
|
290
|
+
// Optional property (?) is treated as nullable
|
|
291
291
|
if (property.optional) {
|
|
292
292
|
isNullable = true;
|
|
293
293
|
}
|
|
294
|
-
//
|
|
294
|
+
// Handle null/undefined in union types
|
|
295
295
|
if (baseTypeNode?.type === utils_1.AST_NODE_TYPES.TSUnionType) {
|
|
296
296
|
const hasNullish = baseTypeNode.types.some((t) => {
|
|
297
297
|
return (t.type === utils_1.AST_NODE_TYPES.TSNullKeyword ||
|
|
@@ -305,7 +305,7 @@ exports.default = (0, createRule_1.createRule)({
|
|
|
305
305
|
t.type !== utils_1.AST_NODE_TYPES.TSUndefinedKeyword);
|
|
306
306
|
}) ?? null;
|
|
307
307
|
}
|
|
308
|
-
//
|
|
308
|
+
// First unwrap Ref<T> and Opt<T> → T, and handle null/undefined within
|
|
309
309
|
if (baseTypeNode?.type === utils_1.AST_NODE_TYPES.TSTypeReference &&
|
|
310
310
|
baseTypeNode.typeName.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
311
311
|
(baseTypeNode.typeName.name === "Ref" ||
|
|
@@ -326,11 +326,11 @@ exports.default = (0, createRule_1.createRule)({
|
|
|
326
326
|
}
|
|
327
327
|
baseTypeNode = inner ?? baseTypeNode;
|
|
328
328
|
}
|
|
329
|
-
//
|
|
329
|
+
// Skip Collection<T> types (these should be handled by OneToMany etc. decorators)
|
|
330
330
|
if (baseTypeNode && isCollectionType(baseTypeNode)) {
|
|
331
331
|
return null;
|
|
332
332
|
}
|
|
333
|
-
//
|
|
333
|
+
// Array type (T[] or Array<T>) - checked after unwrapping Opt/Ref
|
|
334
334
|
const elementTypeNode = baseTypeNode
|
|
335
335
|
? extractArrayElementType(baseTypeNode)
|
|
336
336
|
: null;
|
|
@@ -338,11 +338,11 @@ exports.default = (0, createRule_1.createRule)({
|
|
|
338
338
|
isArray = true;
|
|
339
339
|
}
|
|
340
340
|
const targetTypeNode = elementTypeNode ?? baseTypeNode;
|
|
341
|
-
//
|
|
341
|
+
// Check if target type is an enum
|
|
342
342
|
if (targetTypeNode) {
|
|
343
343
|
isEnum = isEnumType(targetTypeNode);
|
|
344
344
|
}
|
|
345
|
-
//
|
|
345
|
+
// When no explicit type, try to infer from literal initializer
|
|
346
346
|
if (!targetTypeNode) {
|
|
347
347
|
if (property.value?.type === utils_1.AST_NODE_TYPES.Literal) {
|
|
348
348
|
const value = property.value.value;
|
|
@@ -390,7 +390,7 @@ exports.default = (0, createRule_1.createRule)({
|
|
|
390
390
|
arrayElementTypeName: isArray ? "Record" : undefined,
|
|
391
391
|
};
|
|
392
392
|
}
|
|
393
|
-
//
|
|
393
|
+
// Keyword types
|
|
394
394
|
if (targetTypeNode.type === utils_1.AST_NODE_TYPES.TSStringKeyword) {
|
|
395
395
|
return {
|
|
396
396
|
typeName: "string",
|
|
@@ -421,10 +421,10 @@ exports.default = (0, createRule_1.createRule)({
|
|
|
421
421
|
arrayElementTypeName: isArray ? "boolean" : undefined,
|
|
422
422
|
};
|
|
423
423
|
}
|
|
424
|
-
//
|
|
424
|
+
// Identifier (class/custom type)
|
|
425
425
|
const ident = getIdentifierName(targetTypeNode);
|
|
426
426
|
if (ident) {
|
|
427
|
-
//
|
|
427
|
+
// For arrays, custom type arrays use t.json
|
|
428
428
|
if (isArray) {
|
|
429
429
|
return {
|
|
430
430
|
typeName: ident,
|
|
@@ -446,7 +446,7 @@ exports.default = (0, createRule_1.createRule)({
|
|
|
446
446
|
}
|
|
447
447
|
return null;
|
|
448
448
|
};
|
|
449
|
-
//
|
|
449
|
+
// Wrap type with Opt<T>
|
|
450
450
|
const wrapWithOpt = (typeString) => {
|
|
451
451
|
return `Opt<${typeString}>`;
|
|
452
452
|
};
|
|
@@ -465,11 +465,11 @@ exports.default = (0, createRule_1.createRule)({
|
|
|
465
465
|
};
|
|
466
466
|
const buildPropertyDecorator = (info, otherProps = [], decoratorName = "Property") => {
|
|
467
467
|
const options = [];
|
|
468
|
-
//
|
|
468
|
+
// If there is a propertyType configuration, add type
|
|
469
469
|
if (info.propertyType) {
|
|
470
470
|
options.push(`type: ${info.propertyType}`);
|
|
471
471
|
}
|
|
472
|
-
//
|
|
472
|
+
// Add other properties (keeping original order)
|
|
473
473
|
for (const prop of otherProps) {
|
|
474
474
|
options.push(`${prop.key}: ${prop.value}`);
|
|
475
475
|
}
|
|
@@ -493,36 +493,36 @@ exports.default = (0, createRule_1.createRule)({
|
|
|
493
493
|
};
|
|
494
494
|
const fixWithTypeInfo = (property, propertyDecorator, info, currentConfig, decoratorName = "Property") => {
|
|
495
495
|
const fixes = [];
|
|
496
|
-
//
|
|
496
|
+
// If current config already has a valid value, keep it instead of replacing with default
|
|
497
497
|
const finalInfo = { ...info };
|
|
498
|
-
// PrimaryKey
|
|
498
|
+
// PrimaryKey number type defaults to t.integer
|
|
499
499
|
if (decoratorName === "PrimaryKey" &&
|
|
500
500
|
finalInfo.propertyType === "t.float") {
|
|
501
501
|
finalInfo.propertyType = "t.integer";
|
|
502
502
|
}
|
|
503
503
|
if (info.propertyType === "t.string" &&
|
|
504
504
|
isValidStringType(currentConfig.type)) {
|
|
505
|
-
//
|
|
505
|
+
// Keep valid string type config
|
|
506
506
|
finalInfo.propertyType = currentConfig.type;
|
|
507
507
|
}
|
|
508
508
|
else if ((info.propertyType === "t.float" ||
|
|
509
509
|
info.propertyType === "t.integer") &&
|
|
510
510
|
isValidNumberType(currentConfig.type)) {
|
|
511
|
-
//
|
|
511
|
+
// Keep valid number type config
|
|
512
512
|
finalInfo.propertyType = currentConfig.type;
|
|
513
513
|
}
|
|
514
514
|
else if (info.propertyType === "t.datetime" &&
|
|
515
515
|
isValidDateType(currentConfig.type)) {
|
|
516
|
-
//
|
|
516
|
+
// Keep valid Date type config
|
|
517
517
|
finalInfo.propertyType = currentConfig.type;
|
|
518
518
|
}
|
|
519
519
|
else if (info.isArray &&
|
|
520
520
|
info.arrayElementTypeName === "number" &&
|
|
521
521
|
isValidNumberArrayType(currentConfig.type)) {
|
|
522
|
-
//
|
|
522
|
+
// Keep valid number[] type config (VectorType)
|
|
523
523
|
finalInfo.propertyType = currentConfig.type;
|
|
524
524
|
}
|
|
525
|
-
//
|
|
525
|
+
// Keep other property configs
|
|
526
526
|
const newDecoratorText = buildPropertyDecorator(finalInfo, currentConfig.otherProps, decoratorName);
|
|
527
527
|
fixes.push({
|
|
528
528
|
type: "replace",
|
|
@@ -572,7 +572,7 @@ exports.default = (0, createRule_1.createRule)({
|
|
|
572
572
|
}
|
|
573
573
|
}
|
|
574
574
|
else {
|
|
575
|
-
//
|
|
575
|
+
// Keep all other properties
|
|
576
576
|
otherProps.push({
|
|
577
577
|
key: prop.key.name,
|
|
578
578
|
value: source.getText(prop.value),
|
|
@@ -610,38 +610,38 @@ exports.default = (0, createRule_1.createRule)({
|
|
|
610
610
|
}
|
|
611
611
|
return { items, nullable };
|
|
612
612
|
};
|
|
613
|
-
//
|
|
613
|
+
// Check if the file uses the Opt type but hasn't imported it
|
|
614
614
|
const checkOptUsageWithoutImport = (node) => {
|
|
615
615
|
let usesOpt = false;
|
|
616
|
-
//
|
|
616
|
+
// Iterate through all members to check for Opt<T> type annotations
|
|
617
617
|
node.body.body.forEach((member) => {
|
|
618
618
|
if (member.type !== utils_1.AST_NODE_TYPES.PropertyDefinition)
|
|
619
619
|
return;
|
|
620
620
|
const typeAnnotation = member.typeAnnotation?.typeAnnotation;
|
|
621
621
|
if (!typeAnnotation)
|
|
622
622
|
return;
|
|
623
|
-
//
|
|
623
|
+
// Recursively check if the type node contains Opt
|
|
624
624
|
const containsOpt = (typeNode) => {
|
|
625
625
|
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
|
626
626
|
if (!typeNode)
|
|
627
627
|
return false;
|
|
628
|
-
//
|
|
628
|
+
// Check if it's Opt<T>
|
|
629
629
|
if (typeNode.type === utils_1.AST_NODE_TYPES.TSTypeReference &&
|
|
630
630
|
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
|
631
631
|
typeNode.typeName?.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
632
632
|
typeNode.typeName.name === "Opt") {
|
|
633
633
|
return true;
|
|
634
634
|
}
|
|
635
|
-
//
|
|
635
|
+
// Recursively check union types
|
|
636
636
|
if (typeNode.type === utils_1.AST_NODE_TYPES.TSUnionType) {
|
|
637
637
|
return typeNode.types.some((t) => containsOpt(t));
|
|
638
638
|
}
|
|
639
|
-
//
|
|
639
|
+
// Recursively check type parameters
|
|
640
640
|
if (typeNode.type === utils_1.AST_NODE_TYPES.TSTypeReference &&
|
|
641
641
|
typeNode.typeArguments?.params) {
|
|
642
642
|
return typeNode.typeArguments.params.some((param) => containsOpt(param));
|
|
643
643
|
}
|
|
644
|
-
//
|
|
644
|
+
// Recursively check array element type
|
|
645
645
|
if (typeNode.type === utils_1.AST_NODE_TYPES.TSArrayType) {
|
|
646
646
|
return containsOpt(typeNode.elementType);
|
|
647
647
|
}
|
|
@@ -651,7 +651,7 @@ exports.default = (0, createRule_1.createRule)({
|
|
|
651
651
|
usesOpt = true;
|
|
652
652
|
}
|
|
653
653
|
});
|
|
654
|
-
//
|
|
654
|
+
// If Opt is used but not imported, report an error
|
|
655
655
|
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
|
656
656
|
if (usesOpt && !hasOptImport()) {
|
|
657
657
|
context.report({
|
|
@@ -668,12 +668,12 @@ exports.default = (0, createRule_1.createRule)({
|
|
|
668
668
|
ClassDeclaration(node) {
|
|
669
669
|
if (!isEntityClass(node))
|
|
670
670
|
return;
|
|
671
|
-
//
|
|
671
|
+
// First check if Opt is used but not imported
|
|
672
672
|
checkOptUsageWithoutImport(node);
|
|
673
673
|
node.body.body.forEach((member) => {
|
|
674
674
|
if (member.type !== utils_1.AST_NODE_TYPES.PropertyDefinition)
|
|
675
675
|
return;
|
|
676
|
-
//
|
|
676
|
+
// Check relation decorators (OneToOne, OneToMany, ManyToOne, ManyToMany)
|
|
677
677
|
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
|
678
678
|
const hasRelationDecorator = member.decorators?.some((decorator) => {
|
|
679
679
|
if (decorator.expression.type === utils_1.AST_NODE_TYPES.CallExpression &&
|
|
@@ -688,17 +688,17 @@ exports.default = (0, createRule_1.createRule)({
|
|
|
688
688
|
}
|
|
689
689
|
return false;
|
|
690
690
|
});
|
|
691
|
-
//
|
|
691
|
+
// If there are relation decorators, skip checking (these properties don't need @Property)
|
|
692
692
|
if (hasRelationDecorator)
|
|
693
693
|
return;
|
|
694
|
-
//
|
|
694
|
+
// Property-like decorators (decorators that need type checking)
|
|
695
695
|
const propertyLikeDecorators = [
|
|
696
696
|
"Property",
|
|
697
697
|
"PrimaryKey",
|
|
698
698
|
"EncryptedProperty",
|
|
699
699
|
"HashedProperty",
|
|
700
700
|
];
|
|
701
|
-
//
|
|
701
|
+
// Find property-like decorator
|
|
702
702
|
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
|
703
703
|
const propertyLikeDecorator = member.decorators?.find((decorator) => {
|
|
704
704
|
if (decorator.expression.type === utils_1.AST_NODE_TYPES.CallExpression &&
|
|
@@ -707,7 +707,7 @@ exports.default = (0, createRule_1.createRule)({
|
|
|
707
707
|
}
|
|
708
708
|
return false;
|
|
709
709
|
});
|
|
710
|
-
//
|
|
710
|
+
// Get decorator name
|
|
711
711
|
const getDecoratorName = (decorator) => {
|
|
712
712
|
if (decorator.expression.type === utils_1.AST_NODE_TYPES.CallExpression &&
|
|
713
713
|
decorator.expression.callee.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
@@ -715,7 +715,7 @@ exports.default = (0, createRule_1.createRule)({
|
|
|
715
715
|
}
|
|
716
716
|
return null;
|
|
717
717
|
};
|
|
718
|
-
//
|
|
718
|
+
// Check @Enum decorator
|
|
719
719
|
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
|
720
720
|
const enumDecorator = member.decorators?.find((decorator) => {
|
|
721
721
|
return (decorator.expression.type === utils_1.AST_NODE_TYPES.CallExpression &&
|
|
@@ -723,7 +723,7 @@ exports.default = (0, createRule_1.createRule)({
|
|
|
723
723
|
utils_1.AST_NODE_TYPES.Identifier &&
|
|
724
724
|
decorator.expression.callee.name === "Enum");
|
|
725
725
|
});
|
|
726
|
-
//
|
|
726
|
+
// Use property-like decorator
|
|
727
727
|
const propertyDecorator = propertyLikeDecorator;
|
|
728
728
|
const currentDecoratorName = propertyDecorator
|
|
729
729
|
? getDecoratorName(propertyDecorator)
|
|
@@ -731,20 +731,20 @@ exports.default = (0, createRule_1.createRule)({
|
|
|
731
731
|
const typeInfo = computeTypeInfo(member);
|
|
732
732
|
if (!typeInfo?.typeName)
|
|
733
733
|
return;
|
|
734
|
-
//
|
|
734
|
+
// Check initializer and Opt<T> type match
|
|
735
735
|
const hasInitializer = member.value !== null;
|
|
736
736
|
const isOptWrapped = isWrappedWithOpt(member);
|
|
737
|
-
//
|
|
737
|
+
// Check if the initializer is null
|
|
738
738
|
const isInitializedToNull = hasInitializer &&
|
|
739
739
|
member.value?.type === utils_1.AST_NODE_TYPES.Literal &&
|
|
740
740
|
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
|
741
741
|
member.value?.value === null;
|
|
742
|
-
//
|
|
742
|
+
// Case 1: Has non-null initializer but not wrapped with Opt<T>
|
|
743
743
|
if (hasInitializer &&
|
|
744
744
|
!isInitializedToNull &&
|
|
745
745
|
!isOptWrapped &&
|
|
746
746
|
!member.optional) {
|
|
747
|
-
//
|
|
747
|
+
// Has initializer but not wrapped with Opt<T>, needs to report error
|
|
748
748
|
const typeAnnotation = member.typeAnnotation?.typeAnnotation;
|
|
749
749
|
const needsImport = !hasOptImport();
|
|
750
750
|
if (typeAnnotation) {
|
|
@@ -757,7 +757,7 @@ exports.default = (0, createRule_1.createRule)({
|
|
|
757
757
|
const fixes = [
|
|
758
758
|
fixer.replaceText(typeAnnotation, wrappedType),
|
|
759
759
|
];
|
|
760
|
-
//
|
|
760
|
+
// If needed, add Opt import
|
|
761
761
|
if (needsImport) {
|
|
762
762
|
const importFix = addOptImport(fixer);
|
|
763
763
|
if (importFix)
|
|
@@ -768,7 +768,7 @@ exports.default = (0, createRule_1.createRule)({
|
|
|
768
768
|
});
|
|
769
769
|
}
|
|
770
770
|
else if (member.value) {
|
|
771
|
-
//
|
|
771
|
+
// No type annotation, infer type from initializer
|
|
772
772
|
let inferredType = null;
|
|
773
773
|
if (member.value.type === utils_1.AST_NODE_TYPES.Literal) {
|
|
774
774
|
const valueType = typeof member.value.value;
|
|
@@ -780,11 +780,11 @@ exports.default = (0, createRule_1.createRule)({
|
|
|
780
780
|
inferredType = "string";
|
|
781
781
|
}
|
|
782
782
|
else if (member.value.type === utils_1.AST_NODE_TYPES.ArrayExpression) {
|
|
783
|
-
//
|
|
783
|
+
// Empty array [] case, need to infer type from @Property decorator
|
|
784
784
|
inferredType = "unknown[]";
|
|
785
785
|
}
|
|
786
786
|
else if (member.value.type === utils_1.AST_NODE_TYPES.NewExpression) {
|
|
787
|
-
// new Date()
|
|
787
|
+
// new Date() case
|
|
788
788
|
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
|
789
789
|
if (member.value.callee?.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
790
790
|
inferredType = member.value.callee.name;
|
|
@@ -800,7 +800,7 @@ exports.default = (0, createRule_1.createRule)({
|
|
|
800
800
|
const fixes = [
|
|
801
801
|
fixer.insertTextAfter(propertyName, `: ${wrappedType}`),
|
|
802
802
|
];
|
|
803
|
-
//
|
|
803
|
+
// If needed, add Opt import
|
|
804
804
|
if (needsImport) {
|
|
805
805
|
const importFix = addOptImport(fixer);
|
|
806
806
|
if (importFix)
|
|
@@ -812,10 +812,10 @@ exports.default = (0, createRule_1.createRule)({
|
|
|
812
812
|
}
|
|
813
813
|
}
|
|
814
814
|
}
|
|
815
|
-
//
|
|
815
|
+
// If there is an @Enum decorator, check its configuration regardless of whether the type is recognized as enum
|
|
816
816
|
if (enumDecorator) {
|
|
817
817
|
const enumConfig = parseEnumDecorator(enumDecorator);
|
|
818
|
-
//
|
|
818
|
+
// Check if nullable configuration matches the TypeScript type
|
|
819
819
|
if (enumConfig.nullable !== typeInfo.isNullable) {
|
|
820
820
|
const expectedEnumText = buildEnumDecorator(typeInfo);
|
|
821
821
|
context.report({
|
|
@@ -826,12 +826,12 @@ exports.default = (0, createRule_1.createRule)({
|
|
|
826
826
|
},
|
|
827
827
|
});
|
|
828
828
|
}
|
|
829
|
-
//
|
|
829
|
+
// Properties with @Enum decorator do not need @Property decorator
|
|
830
830
|
return;
|
|
831
831
|
}
|
|
832
|
-
//
|
|
832
|
+
// If it's an enum type but doesn't have @Enum decorator
|
|
833
833
|
if (typeInfo.isEnum) {
|
|
834
|
-
//
|
|
834
|
+
// If it has @Property decorator, suggest replacing with @Enum
|
|
835
835
|
if (propertyDecorator) {
|
|
836
836
|
context.report({
|
|
837
837
|
node: propertyDecorator,
|
|
@@ -843,7 +843,7 @@ exports.default = (0, createRule_1.createRule)({
|
|
|
843
843
|
});
|
|
844
844
|
return;
|
|
845
845
|
}
|
|
846
|
-
//
|
|
846
|
+
// If it doesn't have @Enum decorator, add one
|
|
847
847
|
context.report({
|
|
848
848
|
node: member,
|
|
849
849
|
messageId: "useEnumDecorator",
|
|
@@ -854,7 +854,7 @@ exports.default = (0, createRule_1.createRule)({
|
|
|
854
854
|
});
|
|
855
855
|
return;
|
|
856
856
|
}
|
|
857
|
-
//
|
|
857
|
+
// Non-enum type: if no @Property decorator, add one
|
|
858
858
|
if (!propertyDecorator) {
|
|
859
859
|
context.report({
|
|
860
860
|
node: member,
|
|
@@ -866,39 +866,39 @@ exports.default = (0, createRule_1.createRule)({
|
|
|
866
866
|
});
|
|
867
867
|
return;
|
|
868
868
|
}
|
|
869
|
-
//
|
|
869
|
+
// Check if existing decorator matches the type
|
|
870
870
|
const currentConfig = parsePropertyDecorator(propertyDecorator);
|
|
871
871
|
let expectedType = typeInfo.propertyType;
|
|
872
|
-
// PrimaryKey
|
|
872
|
+
// PrimaryKey number type expects t.integer
|
|
873
873
|
if (currentDecoratorName === "PrimaryKey" &&
|
|
874
874
|
expectedType === "t.float") {
|
|
875
875
|
expectedType = "t.integer";
|
|
876
876
|
}
|
|
877
877
|
let needReport = false;
|
|
878
|
-
//
|
|
878
|
+
// Check type configuration
|
|
879
879
|
if (expectedType && currentConfig.type !== expectedType) {
|
|
880
|
-
//
|
|
880
|
+
// For string type, accept multiple valid configurations
|
|
881
881
|
if (expectedType === "t.string" &&
|
|
882
882
|
isValidStringType(currentConfig.type)) {
|
|
883
|
-
//
|
|
883
|
+
// Current config is a valid string type configuration, no modification needed
|
|
884
884
|
}
|
|
885
885
|
else if ((expectedType === "t.float" || expectedType === "t.integer") &&
|
|
886
886
|
isValidNumberType(currentConfig.type)) {
|
|
887
|
-
//
|
|
887
|
+
// Current config is a valid number type configuration, no modification needed
|
|
888
888
|
}
|
|
889
889
|
else if (expectedType === "t.datetime" &&
|
|
890
890
|
isValidDateType(currentConfig.type)) {
|
|
891
|
-
//
|
|
891
|
+
// Current config is a valid Date type configuration, no modification needed
|
|
892
892
|
}
|
|
893
893
|
else if (expectedType === "t.array" &&
|
|
894
894
|
typeInfo.arrayElementTypeName === "number" &&
|
|
895
895
|
isValidNumberArrayType(currentConfig.type)) {
|
|
896
|
-
//
|
|
896
|
+
// Current config is a valid number[] type configuration (t.array or VectorType), no modification needed
|
|
897
897
|
}
|
|
898
898
|
else if (expectedType === "t.json") {
|
|
899
|
-
//
|
|
899
|
+
// For t.json type (Record or custom type arrays)
|
|
900
900
|
if (currentConfig.type === "t.json") {
|
|
901
|
-
// t.json
|
|
901
|
+
// t.json config is correct, no modification needed
|
|
902
902
|
}
|
|
903
903
|
else {
|
|
904
904
|
needReport = true;
|
|
@@ -909,10 +909,10 @@ exports.default = (0, createRule_1.createRule)({
|
|
|
909
909
|
}
|
|
910
910
|
}
|
|
911
911
|
else if (!expectedType && currentConfig.type) {
|
|
912
|
-
//
|
|
912
|
+
// If type is not needed but currently has one, needs correction
|
|
913
913
|
needReport = true;
|
|
914
914
|
}
|
|
915
|
-
//
|
|
915
|
+
// Check nullable configuration
|
|
916
916
|
if (currentConfig.nullable !== typeInfo.isNullable) {
|
|
917
917
|
needReport = true;
|
|
918
918
|
}
|