@nest-boot/eslint-plugin 7.0.0-beta.1 → 7.0.0-beta.3

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.
Files changed (80) hide show
  1. package/.turbo/turbo-build.log +2 -2
  2. package/CHANGELOG.md +4 -16
  3. package/dist/index.d.ts +3 -0
  4. package/dist/index.js +4 -6
  5. package/dist/index.js.map +1 -1
  6. package/dist/rules/graphql/graphql-field-config-from-types.d.ts +6 -0
  7. package/dist/rules/graphql/graphql-field-config-from-types.js +417 -0
  8. package/dist/rules/graphql/graphql-field-config-from-types.js.map +1 -0
  9. package/dist/rules/graphql/graphql-field-definite-assignment.d.ts +2 -0
  10. package/dist/rules/graphql/graphql-field-definite-assignment.js +125 -0
  11. package/dist/rules/graphql/graphql-field-definite-assignment.js.map +1 -0
  12. package/dist/rules/import/import-bullmq.d.ts +2 -0
  13. package/dist/rules/import/import-bullmq.js +36 -0
  14. package/dist/rules/import/import-bullmq.js.map +1 -0
  15. package/dist/rules/import/import-graphql.d.ts +2 -0
  16. package/dist/rules/import/import-graphql.js +36 -0
  17. package/dist/rules/import/import-graphql.js.map +1 -0
  18. package/dist/rules/import/import-mikro-orm.d.ts +2 -0
  19. package/dist/rules/import/import-mikro-orm.js +36 -0
  20. package/dist/rules/import/import-mikro-orm.js.map +1 -0
  21. package/dist/rules/index.d.ts +9 -0
  22. package/dist/rules/index.js +16 -11
  23. package/dist/rules/index.js.map +1 -1
  24. package/dist/rules/mikro-orm/entity-field-definite-assignment.d.ts +2 -0
  25. package/dist/rules/mikro-orm/entity-field-definite-assignment.js +125 -0
  26. package/dist/rules/mikro-orm/entity-field-definite-assignment.js.map +1 -0
  27. package/dist/rules/mikro-orm/entity-property-config-from-types.d.ts +3 -0
  28. package/dist/rules/mikro-orm/entity-property-config-from-types.js +881 -0
  29. package/dist/rules/mikro-orm/entity-property-config-from-types.js.map +1 -0
  30. package/dist/tsconfig.build.tsbuildinfo +1 -0
  31. package/dist/utils/createRule.d.ts +2 -0
  32. package/dist/utils/createRule.js +1 -1
  33. package/dist/utils/createRule.js.map +1 -1
  34. package/dist/utils/decorators.d.ts +29 -0
  35. package/dist/utils/decorators.js +74 -0
  36. package/dist/utils/decorators.js.map +1 -0
  37. package/dist/utils/tester.d.ts +2 -0
  38. package/dist/utils/tester.js +27 -0
  39. package/dist/utils/tester.js.map +1 -0
  40. package/eslint.config.mjs +28 -2
  41. package/jest.config.ts +12 -0
  42. package/package.json +22 -17
  43. package/src/index.ts +8 -2
  44. package/src/rules/graphql/graphql-field-config-from-types.spec.ts +242 -0
  45. package/src/rules/graphql/graphql-field-config-from-types.ts +557 -0
  46. package/src/rules/graphql/graphql-field-definite-assignment.spec.ts +135 -0
  47. package/src/rules/graphql/graphql-field-definite-assignment.ts +147 -0
  48. package/src/rules/import/import-bullmq.spec.ts +69 -0
  49. package/src/rules/import/import-bullmq.ts +35 -0
  50. package/src/rules/import/import-graphql.spec.ts +65 -0
  51. package/src/rules/import/import-graphql.ts +36 -0
  52. package/src/rules/import/import-mikro-orm.spec.ts +65 -0
  53. package/src/rules/import/import-mikro-orm.ts +36 -0
  54. package/src/rules/index.ts +15 -13
  55. package/src/rules/mikro-orm/entity-field-definite-assignment.spec.ts +91 -0
  56. package/src/rules/mikro-orm/entity-field-definite-assignment.ts +141 -0
  57. package/src/rules/mikro-orm/entity-property-config-from-types.spec.ts +262 -0
  58. package/src/rules/mikro-orm/entity-property-config-from-types.ts +1111 -0
  59. package/src/utils/createRule.ts +3 -1
  60. package/src/utils/decorators.spec.ts +214 -0
  61. package/src/utils/decorators.ts +93 -0
  62. package/src/utils/tester.ts +22 -0
  63. package/tsconfig.build.json +5 -0
  64. package/tsconfig.json +6 -7
  65. package/dist/rules/entity-constructor.js +0 -78
  66. package/dist/rules/entity-constructor.js.map +0 -1
  67. package/dist/rules/entity-property-no-optional-or-non-null-assertion.js +0 -63
  68. package/dist/rules/entity-property-no-optional-or-non-null-assertion.js.map +0 -1
  69. package/dist/rules/entity-property-nullable.js +0 -81
  70. package/dist/rules/entity-property-nullable.js.map +0 -1
  71. package/dist/rules/graphql-field-arguments-match-property-type.js +0 -118
  72. package/dist/rules/graphql-field-arguments-match-property-type.js.map +0 -1
  73. package/dist/rules/graphql-resolver-method-return-type.js +0 -145
  74. package/dist/rules/graphql-resolver-method-return-type.js.map +0 -1
  75. package/dist/tsconfig.tsbuildinfo +0 -1
  76. package/src/rules/entity-constructor.ts +0 -97
  77. package/src/rules/entity-property-no-optional-or-non-null-assertion.ts +0 -81
  78. package/src/rules/entity-property-nullable.ts +0 -112
  79. package/src/rules/graphql-field-arguments-match-property-type.ts +0 -186
  80. package/src/rules/graphql-resolver-method-return-type.ts +0 -207
@@ -0,0 +1,1111 @@
1
+ import {
2
+ AST_NODE_TYPES,
3
+ ESLintUtils,
4
+ TSESTree,
5
+ } from "@typescript-eslint/utils";
6
+ import type { RuleFix, RuleFixer } from "@typescript-eslint/utils/ts-eslint";
7
+ import * as ts from "typescript";
8
+
9
+ import { createRule } from "../../utils/createRule";
10
+ import { hasClassDecorator } from "../../utils/decorators";
11
+
12
+ // 自定义 Fix 对象类型,用于延迟应用修复
13
+ interface CustomFix {
14
+ type: "insert" | "replace";
15
+ range: readonly [number, number];
16
+ text: string;
17
+ }
18
+
19
+ interface TypeInfo {
20
+ typeName: string | null;
21
+ isArray: boolean;
22
+ isNullable: boolean;
23
+ isEnum: boolean; // 是否是枚举类型
24
+ propertyType: string | null; // MikroORM 类型如 t.text, t.bigint 等
25
+ arrayElementTypeName?: string | null; // 数组元素的类型名称(用于特殊验证)
26
+ }
27
+
28
+ export default createRule<
29
+ [],
30
+ | "alignPropertyDecoratorWithTsType"
31
+ | "removePropertyDecorator"
32
+ | "useEnumDecorator"
33
+ | "useOptTypeForInitializedProperty"
34
+ | "removeOptTypeForNonInitializedProperty"
35
+ >({
36
+ name: "entity-property-config-from-types",
37
+ meta: {
38
+ type: "problem",
39
+ docs: {
40
+ description:
41
+ "根据 TypeScript 类型自动生成或修正 @Property 装饰器的类型与 nullable 配置(支持数组),以及 @Enum 装饰器的 nullable 配置。检查有初始化值的属性是否使用 Opt<T> 类型。",
42
+ },
43
+ fixable: "code",
44
+ schema: [],
45
+ messages: {
46
+ alignPropertyDecoratorWithTsType:
47
+ "@Property 装饰器应与 TypeScript 类型保持一致(类型与 nullable)。",
48
+ removePropertyDecorator:
49
+ "属性带有 @{{decoratorName}} 装饰器,应移除 @Property 装饰器。",
50
+ useEnumDecorator: "枚举类型应使用 @Enum 装饰器而不是 @Property 装饰器。",
51
+ useOptTypeForInitializedProperty:
52
+ "有非 null 初始化值的属性应使用 Opt<T> 类型包装。",
53
+ removeOptTypeForNonInitializedProperty:
54
+ "没有初始化值或初始化为 null 的属性不应使用 Opt<T> 类型包装。",
55
+ },
56
+ },
57
+ defaultOptions: [],
58
+ create(context) {
59
+ const source = context.sourceCode;
60
+ const parserServices = ESLintUtils.getParserServices(context);
61
+ const checker = parserServices.program.getTypeChecker();
62
+
63
+ // 检查是否为枚举类型
64
+ const isEnumType = (node: TSESTree.Node): boolean => {
65
+ try {
66
+ const tsNode = parserServices.esTreeNodeToTSNodeMap.get(node);
67
+ const type = checker.getTypeAtLocation(tsNode);
68
+
69
+ // 检查是否是枚举类型
70
+ if (type.symbol.flags & ts.SymbolFlags.Enum) {
71
+ return true;
72
+ }
73
+
74
+ // 检查联合类型中的每个成员
75
+ if (type.isUnion()) {
76
+ return type.types.some(
77
+ (t) =>
78
+ t.symbol.flags & ts.SymbolFlags.EnumMember ||
79
+ t.symbol.flags & ts.SymbolFlags.Enum,
80
+ );
81
+ }
82
+
83
+ return false;
84
+ } catch {
85
+ return false;
86
+ }
87
+ };
88
+
89
+ const getPropertyType = (typeName: string | null): string | null => {
90
+ if (!typeName) return null;
91
+
92
+ switch (typeName) {
93
+ case "String":
94
+ case "string":
95
+ return "t.string"; // 默认使用 t.string
96
+ case "Number":
97
+ case "number":
98
+ return "t.float"; // 默认使用 t.float
99
+ case "Boolean":
100
+ case "boolean":
101
+ return "t.boolean"; // 默认使用 t.boolean
102
+ case "Date":
103
+ return null; // Date 类型不需要指定
104
+ case "GraphQLJSONObject":
105
+ case "Record":
106
+ return "t.json";
107
+ default:
108
+ return null;
109
+ }
110
+ };
111
+
112
+ const isValidStringType = (typeConfig: string | null): boolean => {
113
+ if (!typeConfig) return false;
114
+
115
+ // 接受 t.string, t.text, t.decimal, t.bigint
116
+ if (
117
+ typeConfig === "t.string" ||
118
+ typeConfig === "t.text" ||
119
+ typeConfig === "t.decimal" ||
120
+ typeConfig === "t.bigint"
121
+ ) {
122
+ return true;
123
+ }
124
+
125
+ // 接受 DecimalType(类引用,不带参数)
126
+ if (typeConfig === "DecimalType") {
127
+ return true;
128
+ }
129
+
130
+ // 接受 new DecimalType('string') 和 new BigIntType('string')
131
+ // 但排除 new BigIntType('number')
132
+ if (
133
+ typeConfig.includes("DecimalType") ||
134
+ typeConfig.includes("BigIntType")
135
+ ) {
136
+ // 如果是 new BigIntType('number'),则无效
137
+ if (
138
+ typeConfig.includes("BigIntType") &&
139
+ (typeConfig.includes("'number'") || typeConfig.includes('"number"'))
140
+ ) {
141
+ return false;
142
+ }
143
+ // 其他情况(包括 new XXXType('string') 和 DecimalType)都有效
144
+ return true;
145
+ }
146
+
147
+ return false;
148
+ };
149
+
150
+ const isValidNumberType = (typeConfig: string | null): boolean => {
151
+ if (!typeConfig) return false;
152
+
153
+ // 接受 t.integer, t.float, t.double, t.decimal
154
+ if (
155
+ typeConfig === "t.integer" ||
156
+ typeConfig === "t.float" ||
157
+ typeConfig === "t.double" ||
158
+ typeConfig === "t.decimal"
159
+ ) {
160
+ return true;
161
+ }
162
+
163
+ // 接受 BigIntType 和 DecimalType(类引用,不带参数)
164
+ if (typeConfig === "BigIntType" || typeConfig === "DecimalType") {
165
+ return true;
166
+ }
167
+
168
+ // 接受 new DecimalType('number') 和 new BigIntType('number')
169
+ // 但排除 new XXXType('string')
170
+ if (
171
+ typeConfig.includes("DecimalType") ||
172
+ typeConfig.includes("BigIntType")
173
+ ) {
174
+ // 如果包含 'string',则对 number 类型无效
175
+ if (
176
+ typeConfig.includes("'string'") ||
177
+ typeConfig.includes('"string"')
178
+ ) {
179
+ return false;
180
+ }
181
+ // 其他情况(包括 new XXXType('number'))都有效
182
+ return true;
183
+ }
184
+
185
+ return false;
186
+ };
187
+
188
+ const isValidNumberArrayType = (typeConfig: string | null): boolean => {
189
+ if (!typeConfig) return false;
190
+
191
+ // 接受 t.array(默认)
192
+ if (typeConfig === "t.array") {
193
+ return true;
194
+ }
195
+
196
+ // 接受 VectorType(类引用)
197
+ if (typeConfig === "VectorType") {
198
+ return true;
199
+ }
200
+
201
+ // 接受 new VectorType(...)
202
+ if (typeConfig.includes("VectorType")) {
203
+ return true;
204
+ }
205
+
206
+ return false;
207
+ };
208
+
209
+ const getIdentifierName = (node: TSESTree.TypeNode): string | null => {
210
+ if (
211
+ node.type === AST_NODE_TYPES.TSTypeReference &&
212
+ node.typeName.type === AST_NODE_TYPES.Identifier
213
+ ) {
214
+ return node.typeName.name;
215
+ }
216
+ return null;
217
+ };
218
+
219
+ const extractArrayElementType = (
220
+ node: TSESTree.TypeNode,
221
+ ): TSESTree.TypeNode | null => {
222
+ if (node.type === AST_NODE_TYPES.TSArrayType) {
223
+ return node.elementType;
224
+ }
225
+ if (
226
+ node.type === AST_NODE_TYPES.TSTypeReference &&
227
+ node.typeName.type === AST_NODE_TYPES.Identifier &&
228
+ node.typeName.name === "Array"
229
+ ) {
230
+ return node.typeArguments?.params[0] ?? null;
231
+ }
232
+ return null;
233
+ };
234
+
235
+ const isCollectionType = (node: TSESTree.TypeNode): boolean => {
236
+ return (
237
+ node.type === AST_NODE_TYPES.TSTypeReference &&
238
+ node.typeName.type === AST_NODE_TYPES.Identifier &&
239
+ node.typeName.name === "Collection"
240
+ );
241
+ };
242
+
243
+ // 检查类型是否被 Opt<T> 包装
244
+ const isWrappedWithOpt = (
245
+ property: TSESTree.PropertyDefinition,
246
+ ): boolean => {
247
+ const typeAnnotation = property.typeAnnotation;
248
+ if (typeAnnotation?.type !== AST_NODE_TYPES.TSTypeAnnotation) {
249
+ return false;
250
+ }
251
+
252
+ const typeNode = typeAnnotation.typeAnnotation;
253
+ return (
254
+ typeNode.type === AST_NODE_TYPES.TSTypeReference &&
255
+ typeNode.typeName.type === AST_NODE_TYPES.Identifier &&
256
+ typeNode.typeName.name === "Opt"
257
+ );
258
+ };
259
+
260
+ // 检查是否已导入 Opt
261
+ const hasOptImport = (): boolean => {
262
+ const program = context.sourceCode.ast;
263
+ for (const statement of program.body) {
264
+ if (statement.type === AST_NODE_TYPES.ImportDeclaration) {
265
+ const importSource = statement.source.value;
266
+ // 检查从 @mikro-orm/* 导入的语句
267
+ if (
268
+ typeof importSource === "string" &&
269
+ importSource.startsWith("@mikro-orm/")
270
+ ) {
271
+ const hasOpt = statement.specifiers.some(
272
+ (spec: TSESTree.ImportClause) => {
273
+ return (
274
+ spec.type === AST_NODE_TYPES.ImportSpecifier &&
275
+ spec.imported.type === AST_NODE_TYPES.Identifier &&
276
+ spec.imported.name === "Opt"
277
+ );
278
+ },
279
+ );
280
+ if (hasOpt) return true;
281
+ }
282
+ }
283
+ }
284
+ return false;
285
+ };
286
+
287
+ // 添加 Opt 到 @mikro-orm/core 的导入
288
+ const addOptImport = (fixer: RuleFixer): RuleFix | null => {
289
+ const program = context.sourceCode.ast;
290
+
291
+ // 查找 @mikro-orm/core 的导入语句
292
+ let coreImport: TSESTree.ImportDeclaration | null = null;
293
+
294
+ for (const statement of program.body) {
295
+ if (statement.type === AST_NODE_TYPES.ImportDeclaration) {
296
+ const importSource = statement.source.value;
297
+ if (importSource === "@mikro-orm/core") {
298
+ coreImport = statement;
299
+ break;
300
+ }
301
+ }
302
+ }
303
+
304
+ if (coreImport) {
305
+ // 已有 @mikro-orm/core 导入,添加 Opt 到导入列表
306
+ const lastSpecifier =
307
+ coreImport.specifiers[coreImport.specifiers.length - 1];
308
+
309
+ // 检查是否是多行导入
310
+ const importText = source.getText(coreImport);
311
+ const isMultiline = importText.includes("\n");
312
+
313
+ if (isMultiline) {
314
+ // 多行导入:在最后一个导入项后添加,保持缩进
315
+ const indent = " "; // 假设使用 2 个空格缩进
316
+ return fixer.insertTextAfter(lastSpecifier, `,\n${indent}Opt`);
317
+ } else {
318
+ // 单行导入:直接添加
319
+ return fixer.insertTextAfter(lastSpecifier, ", Opt");
320
+ }
321
+ } else {
322
+ // 没有 @mikro-orm/core 导入,在文件开头添加新的导入语句
323
+ const firstImport = program.body.find(
324
+ (node: TSESTree.ProgramStatement) =>
325
+ node.type === AST_NODE_TYPES.ImportDeclaration,
326
+ );
327
+
328
+ if (firstImport) {
329
+ return fixer.insertTextBefore(
330
+ firstImport,
331
+ "import { Opt } from '@mikro-orm/core';\n",
332
+ );
333
+ }
334
+ }
335
+ return null;
336
+ };
337
+
338
+ const computeTypeInfo = (
339
+ property: TSESTree.PropertyDefinition,
340
+ ): TypeInfo | null => {
341
+ let isNullable = false;
342
+ let isArray = false;
343
+ let isEnum = false;
344
+
345
+ let baseTypeNode: TSESTree.TypeNode | null =
346
+ property.typeAnnotation?.type === AST_NODE_TYPES.TSTypeAnnotation
347
+ ? property.typeAnnotation.typeAnnotation
348
+ : null;
349
+
350
+ // 可选属性(?)视为可空
351
+ if (property.optional) {
352
+ isNullable = true;
353
+ }
354
+
355
+ // 处理联合类型中的 null/undefined
356
+ if (baseTypeNode?.type === AST_NODE_TYPES.TSUnionType) {
357
+ const hasNullish = baseTypeNode.types.some((t: TSESTree.TypeNode) => {
358
+ return (
359
+ t.type === AST_NODE_TYPES.TSNullKeyword ||
360
+ t.type === AST_NODE_TYPES.TSUndefinedKeyword
361
+ );
362
+ });
363
+ if (hasNullish) isNullable = true;
364
+
365
+ baseTypeNode =
366
+ baseTypeNode.types.find((t: TSESTree.TypeNode) => {
367
+ return (
368
+ t.type !== AST_NODE_TYPES.TSNullKeyword &&
369
+ t.type !== AST_NODE_TYPES.TSUndefinedKeyword
370
+ );
371
+ }) ?? null;
372
+ }
373
+
374
+ // 先解包 Ref<T> 和 Opt<T> → T,并在内部再次处理 null/undefined
375
+ if (
376
+ baseTypeNode?.type === AST_NODE_TYPES.TSTypeReference &&
377
+ baseTypeNode.typeName.type === AST_NODE_TYPES.Identifier &&
378
+ (baseTypeNode.typeName.name === "Ref" ||
379
+ baseTypeNode.typeName.name === "Opt")
380
+ ) {
381
+ let inner = baseTypeNode.typeArguments?.params[0] ?? null;
382
+ if (inner && inner.type === AST_NODE_TYPES.TSUnionType) {
383
+ const hasNullish = inner.types.some((t: TSESTree.TypeNode) => {
384
+ return (
385
+ t.type === AST_NODE_TYPES.TSNullKeyword ||
386
+ t.type === AST_NODE_TYPES.TSUndefinedKeyword
387
+ );
388
+ });
389
+ if (hasNullish) isNullable = true;
390
+ inner =
391
+ inner.types.find((t: TSESTree.TypeNode) => {
392
+ return (
393
+ t.type !== AST_NODE_TYPES.TSNullKeyword &&
394
+ t.type !== AST_NODE_TYPES.TSUndefinedKeyword
395
+ );
396
+ }) ?? inner;
397
+ }
398
+ baseTypeNode = inner ?? baseTypeNode;
399
+ }
400
+
401
+ // 跳过 Collection<T> 类型(这些应该由 OneToMany 等装饰器处理)
402
+ if (baseTypeNode && isCollectionType(baseTypeNode)) {
403
+ return null;
404
+ }
405
+
406
+ // 数组类型(T[] 或 Array<T>)- 在解包 Opt/Ref 之后检查
407
+ const elementTypeNode = baseTypeNode
408
+ ? extractArrayElementType(baseTypeNode)
409
+ : null;
410
+ if (elementTypeNode) {
411
+ isArray = true;
412
+ }
413
+
414
+ const targetTypeNode: TSESTree.TypeNode | null =
415
+ elementTypeNode ?? baseTypeNode;
416
+
417
+ // 检查目标类型是否为枚举
418
+ if (targetTypeNode) {
419
+ isEnum = isEnumType(targetTypeNode);
420
+ }
421
+
422
+ // 无显式类型时,尝试从字面量初始值推断
423
+ if (!targetTypeNode) {
424
+ if (property.value?.type === AST_NODE_TYPES.Literal) {
425
+ const value = property.value.value;
426
+ const typeOf = typeof value;
427
+ if (typeOf === "boolean")
428
+ return {
429
+ typeName: "boolean",
430
+ isArray,
431
+ isNullable,
432
+ isEnum: false,
433
+ propertyType: isArray ? "t.json" : getPropertyType("boolean"),
434
+ arrayElementTypeName: isArray ? "boolean" : undefined,
435
+ };
436
+ if (typeOf === "number")
437
+ return {
438
+ typeName: "number",
439
+ isArray,
440
+ isNullable,
441
+ isEnum: false,
442
+ propertyType: isArray ? "t.array" : getPropertyType("number"),
443
+ arrayElementTypeName: isArray ? "number" : undefined,
444
+ };
445
+ if (typeOf === "string")
446
+ return {
447
+ typeName: "string",
448
+ isArray,
449
+ isNullable,
450
+ isEnum: false,
451
+ propertyType: isArray ? "t.array" : getPropertyType("string"),
452
+ arrayElementTypeName: isArray ? "string" : undefined,
453
+ };
454
+ }
455
+ return null;
456
+ }
457
+
458
+ // Record<*, *> → t.json
459
+ if (
460
+ targetTypeNode.type === AST_NODE_TYPES.TSTypeReference &&
461
+ targetTypeNode.typeName.type === AST_NODE_TYPES.Identifier &&
462
+ targetTypeNode.typeName.name === "Record"
463
+ ) {
464
+ return {
465
+ typeName: "Record",
466
+ isArray,
467
+ isNullable,
468
+ isEnum: false,
469
+ propertyType: "t.json",
470
+ arrayElementTypeName: isArray ? "Record" : undefined,
471
+ };
472
+ }
473
+
474
+ // 关键字类型
475
+ if (targetTypeNode.type === AST_NODE_TYPES.TSStringKeyword) {
476
+ return {
477
+ typeName: "string",
478
+ isArray,
479
+ isNullable,
480
+ isEnum: false,
481
+ propertyType: isArray ? "t.array" : getPropertyType("string"),
482
+ arrayElementTypeName: isArray ? "string" : undefined,
483
+ };
484
+ }
485
+ if (targetTypeNode.type === AST_NODE_TYPES.TSNumberKeyword) {
486
+ return {
487
+ typeName: "number",
488
+ isArray,
489
+ isNullable,
490
+ isEnum: false,
491
+ propertyType: isArray ? "t.array" : getPropertyType("number"),
492
+ arrayElementTypeName: isArray ? "number" : undefined,
493
+ };
494
+ }
495
+ if (targetTypeNode.type === AST_NODE_TYPES.TSBooleanKeyword) {
496
+ return {
497
+ typeName: "boolean",
498
+ isArray,
499
+ isNullable,
500
+ isEnum: false,
501
+ propertyType: isArray ? "t.json" : getPropertyType("boolean"),
502
+ arrayElementTypeName: isArray ? "boolean" : undefined,
503
+ };
504
+ }
505
+
506
+ // 标识符(类/自定义类型)
507
+ const ident = getIdentifierName(targetTypeNode);
508
+ if (ident) {
509
+ // 如果是数组,自定义类型数组使用 t.json
510
+ if (isArray) {
511
+ return {
512
+ typeName: ident,
513
+ isArray,
514
+ isNullable,
515
+ isEnum,
516
+ propertyType: "t.json",
517
+ arrayElementTypeName: ident,
518
+ };
519
+ }
520
+
521
+ return {
522
+ typeName: ident,
523
+ isArray,
524
+ isNullable,
525
+ isEnum,
526
+ propertyType: getPropertyType(ident),
527
+ arrayElementTypeName: undefined,
528
+ };
529
+ }
530
+
531
+ return null;
532
+ };
533
+
534
+ // 将类型包装为 Opt<T>
535
+ const wrapWithOpt = (typeString: string): string => {
536
+ return `Opt<${typeString}>`;
537
+ };
538
+
539
+ const buildEnumDecorator = (info: TypeInfo): string => {
540
+ const options: string[] = [];
541
+
542
+ if (info.typeName) {
543
+ options.push(`items: () => ${info.typeName}`);
544
+ }
545
+
546
+ if (info.isNullable) {
547
+ options.push("nullable: true");
548
+ }
549
+
550
+ if (options.length === 0) {
551
+ return "@Enum()";
552
+ }
553
+
554
+ return `@Enum({ ${options.join(", ")} })`;
555
+ };
556
+
557
+ const buildPropertyDecorator = (
558
+ info: TypeInfo,
559
+ otherProps: { key: string; value: string }[] = [],
560
+ ): string => {
561
+ const options: string[] = [];
562
+
563
+ // 如果有 propertyType 配置,添加 type
564
+ if (info.propertyType) {
565
+ options.push(`type: ${info.propertyType}`);
566
+ }
567
+
568
+ // 添加其他属性(保持原有顺序)
569
+ for (const prop of otherProps) {
570
+ options.push(`${prop.key}: ${prop.value}`);
571
+ }
572
+
573
+ if (info.isNullable) {
574
+ options.push("nullable: true");
575
+ }
576
+
577
+ if (options.length === 0) {
578
+ return "@Property()";
579
+ }
580
+
581
+ return `@Property({ ${options.join(", ")} })`;
582
+ };
583
+
584
+ const addPropertyDecorator = (
585
+ property: TSESTree.PropertyDefinition,
586
+ info: TypeInfo,
587
+ ) => {
588
+ const fixes: CustomFix[] = [];
589
+
590
+ const newDecoratorText = buildPropertyDecorator(info);
591
+
592
+ fixes.push({
593
+ type: "insert",
594
+ range: property.range,
595
+ text: newDecoratorText + "\n ",
596
+ });
597
+
598
+ return fixes;
599
+ };
600
+
601
+ const fixWithTypeInfo = (
602
+ property: TSESTree.PropertyDefinition,
603
+ propertyDecorator: TSESTree.Decorator,
604
+ info: TypeInfo,
605
+ currentConfig: {
606
+ type: string | null;
607
+ nullable: boolean;
608
+ otherProps: { key: string; value: string }[];
609
+ },
610
+ ) => {
611
+ const fixes: CustomFix[] = [];
612
+
613
+ // 如果当前已经有有效的配置,保留它而不是替换成默认值
614
+ const finalInfo = { ...info };
615
+ if (
616
+ info.propertyType === "t.string" &&
617
+ isValidStringType(currentConfig.type)
618
+ ) {
619
+ // 保留有效的 string 类型配置
620
+ finalInfo.propertyType = currentConfig.type;
621
+ } else if (
622
+ info.propertyType === "t.float" &&
623
+ isValidNumberType(currentConfig.type)
624
+ ) {
625
+ // 保留有效的 number 类型配置
626
+ finalInfo.propertyType = currentConfig.type;
627
+ } else if (
628
+ info.isArray &&
629
+ info.arrayElementTypeName === "number" &&
630
+ isValidNumberArrayType(currentConfig.type)
631
+ ) {
632
+ // 保留有效的 number[] 类型配置(VectorType)
633
+ finalInfo.propertyType = currentConfig.type;
634
+ }
635
+
636
+ // 保留其他属性配置
637
+ const newDecoratorText = buildPropertyDecorator(
638
+ finalInfo,
639
+ currentConfig.otherProps,
640
+ );
641
+
642
+ fixes.push({
643
+ type: "replace",
644
+ range: propertyDecorator.range,
645
+ text: newDecoratorText,
646
+ });
647
+
648
+ return fixes;
649
+ };
650
+
651
+ const applyFixes = (fixer: RuleFixer, fixes: CustomFix[]) => {
652
+ return fixes.map((fix) => {
653
+ if (fix.type === "replace") {
654
+ return fixer.replaceTextRange(fix.range, fix.text);
655
+ } else {
656
+ return fixer.insertTextBeforeRange(fix.range, fix.text);
657
+ }
658
+ });
659
+ };
660
+
661
+ const isEntityClass = (node: TSESTree.ClassDeclaration): boolean => {
662
+ return hasClassDecorator(node, "Entity");
663
+ };
664
+
665
+ const parsePropertyDecorator = (
666
+ decorator: TSESTree.Decorator,
667
+ ): {
668
+ type: string | null;
669
+ nullable: boolean;
670
+ otherProps: { key: string; value: string }[];
671
+ } => {
672
+ const callExpr = decorator.expression;
673
+ if (
674
+ callExpr.type !== AST_NODE_TYPES.CallExpression ||
675
+ callExpr.arguments.length === 0
676
+ ) {
677
+ return { type: null, nullable: false, otherProps: [] };
678
+ }
679
+
680
+ const firstArg = callExpr.arguments[0];
681
+ if (firstArg.type !== AST_NODE_TYPES.ObjectExpression) {
682
+ return { type: null, nullable: false, otherProps: [] };
683
+ }
684
+
685
+ let type: string | null = null;
686
+ let nullable = false;
687
+ const otherProps: { key: string; value: string }[] = [];
688
+
689
+ for (const prop of firstArg.properties) {
690
+ if (prop.type !== AST_NODE_TYPES.Property) continue;
691
+ if (prop.key.type !== AST_NODE_TYPES.Identifier || !prop.key.name)
692
+ continue;
693
+
694
+ if (prop.key.name === "type") {
695
+ type = source.getText(prop.value);
696
+ } else if (prop.key.name === "nullable") {
697
+ if (
698
+ prop.value.type === AST_NODE_TYPES.Literal &&
699
+ prop.value.value === true
700
+ ) {
701
+ nullable = true;
702
+ }
703
+ } else {
704
+ // 保留其他所有属性
705
+ otherProps.push({
706
+ key: prop.key.name,
707
+ value: source.getText(prop.value),
708
+ });
709
+ }
710
+ }
711
+
712
+ return { type, nullable, otherProps };
713
+ };
714
+
715
+ const parseEnumDecorator = (
716
+ decorator: TSESTree.Decorator,
717
+ ): {
718
+ items: string | null;
719
+ nullable: boolean;
720
+ } => {
721
+ const callExpr = decorator.expression;
722
+ if (
723
+ callExpr.type !== AST_NODE_TYPES.CallExpression ||
724
+ callExpr.arguments.length === 0
725
+ ) {
726
+ return { items: null, nullable: false };
727
+ }
728
+
729
+ const firstArg = callExpr.arguments[0];
730
+ if (firstArg.type !== AST_NODE_TYPES.ObjectExpression) {
731
+ return { items: null, nullable: false };
732
+ }
733
+
734
+ let items: string | null = null;
735
+ let nullable = false;
736
+
737
+ for (const prop of firstArg.properties) {
738
+ if (prop.type !== AST_NODE_TYPES.Property) continue;
739
+ if (prop.key.type !== AST_NODE_TYPES.Identifier || !prop.key.name)
740
+ continue;
741
+
742
+ if (prop.key.name === "items") {
743
+ items = source.getText(prop.value);
744
+ } else if (prop.key.name === "nullable") {
745
+ if (
746
+ prop.value.type === AST_NODE_TYPES.Literal &&
747
+ prop.value.value === true
748
+ ) {
749
+ nullable = true;
750
+ }
751
+ }
752
+ }
753
+
754
+ return { items, nullable };
755
+ };
756
+
757
+ // 检查文件中是否使用了 Opt 类型但没有导入
758
+ const checkOptUsageWithoutImport = (node: TSESTree.ClassDeclaration) => {
759
+ let usesOpt = false;
760
+
761
+ // 遍历所有成员,检查是否有使用 Opt<T> 的类型注解
762
+ node.body.body.forEach((member: TSESTree.ClassElement) => {
763
+ if (member.type !== AST_NODE_TYPES.PropertyDefinition) return;
764
+
765
+ const typeAnnotation = member.typeAnnotation?.typeAnnotation;
766
+ if (!typeAnnotation) return;
767
+
768
+ // 递归检查类型节点中是否包含 Opt
769
+ const containsOpt = (typeNode: TSESTree.TypeNode): boolean => {
770
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
771
+ if (!typeNode) return false;
772
+
773
+ // 检查是否是 Opt<T>
774
+ if (
775
+ typeNode.type === AST_NODE_TYPES.TSTypeReference &&
776
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
777
+ typeNode.typeName?.type === AST_NODE_TYPES.Identifier &&
778
+ typeNode.typeName.name === "Opt"
779
+ ) {
780
+ return true;
781
+ }
782
+
783
+ // 递归检查联合类型
784
+ if (typeNode.type === AST_NODE_TYPES.TSUnionType) {
785
+ return typeNode.types.some((t: TSESTree.TypeNode) =>
786
+ containsOpt(t),
787
+ );
788
+ }
789
+
790
+ // 递归检查类型参数
791
+ if (
792
+ typeNode.type === AST_NODE_TYPES.TSTypeReference &&
793
+ typeNode.typeArguments?.params
794
+ ) {
795
+ return typeNode.typeArguments.params.some(
796
+ (param: TSESTree.TypeNode) => containsOpt(param),
797
+ );
798
+ }
799
+
800
+ // 递归检查数组元素类型
801
+ if (typeNode.type === AST_NODE_TYPES.TSArrayType) {
802
+ return containsOpt(typeNode.elementType);
803
+ }
804
+
805
+ return false;
806
+ };
807
+
808
+ if (containsOpt(typeAnnotation)) {
809
+ usesOpt = true;
810
+ }
811
+ });
812
+
813
+ // 如果使用了 Opt 但没有导入,报告错误
814
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
815
+ if (usesOpt && !hasOptImport()) {
816
+ context.report({
817
+ node,
818
+ messageId: "useOptTypeForInitializedProperty",
819
+ fix: (fixer) => {
820
+ const importFix = addOptImport(fixer);
821
+ return importFix ? [importFix] : [];
822
+ },
823
+ });
824
+ }
825
+ };
826
+
827
+ return {
828
+ ClassDeclaration(node) {
829
+ if (!isEntityClass(node)) return;
830
+
831
+ // 首先检查是否使用了 Opt 但没有导入
832
+ checkOptUsageWithoutImport(node);
833
+
834
+ node.body.body.forEach((member: TSESTree.ClassElement) => {
835
+ if (member.type !== AST_NODE_TYPES.PropertyDefinition) return;
836
+
837
+ // 检查关系装饰器(PrimaryKey, OneToOne, OneToMany, ManyToOne, ManyToMany)
838
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
839
+ const hasRelationDecorator = member.decorators?.some(
840
+ (decorator: TSESTree.Decorator) => {
841
+ if (
842
+ decorator.expression.type === AST_NODE_TYPES.CallExpression &&
843
+ decorator.expression.callee.type === AST_NODE_TYPES.Identifier
844
+ ) {
845
+ const decoratorName = decorator.expression.callee.name;
846
+ return [
847
+ "PrimaryKey",
848
+ "OneToOne",
849
+ "OneToMany",
850
+ "ManyToOne",
851
+ "ManyToMany",
852
+ ].includes(decoratorName);
853
+ }
854
+ return false;
855
+ },
856
+ );
857
+
858
+ // 如果有关系装饰器,跳过检查(这些属性不需要 @Property 装饰器)
859
+ if (hasRelationDecorator) return;
860
+
861
+ // 检查 @Enum 装饰器
862
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
863
+ const enumDecorator = member.decorators?.find(
864
+ (decorator: TSESTree.Decorator) => {
865
+ return (
866
+ decorator.expression.type === AST_NODE_TYPES.CallExpression &&
867
+ decorator.expression.callee.type ===
868
+ AST_NODE_TYPES.Identifier &&
869
+ decorator.expression.callee.name === "Enum"
870
+ );
871
+ },
872
+ );
873
+
874
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
875
+ const propertyDecorator = member.decorators?.find(
876
+ (decorator: TSESTree.Decorator) => {
877
+ return (
878
+ decorator.expression.type === AST_NODE_TYPES.CallExpression &&
879
+ decorator.expression.callee.type ===
880
+ AST_NODE_TYPES.Identifier &&
881
+ decorator.expression.callee.name === "Property"
882
+ );
883
+ },
884
+ );
885
+
886
+ const typeInfo = computeTypeInfo(member);
887
+
888
+ if (!typeInfo?.typeName) return;
889
+
890
+ // 检查初始化值和 Opt<T> 类型的匹配
891
+ const hasInitializer = member.value !== null;
892
+ const isOptWrapped = isWrappedWithOpt(member);
893
+
894
+ // 检查初始化值是否为 null
895
+
896
+ const isInitializedToNull =
897
+ hasInitializer &&
898
+ member.value?.type === AST_NODE_TYPES.Literal &&
899
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
900
+ member.value?.value === null;
901
+
902
+ // 情况 1:有非 null 初始化值但没有使用 Opt<T>
903
+ if (
904
+ hasInitializer &&
905
+ !isInitializedToNull &&
906
+ !isOptWrapped &&
907
+ !member.optional
908
+ ) {
909
+ // 有初始化值但没有使用 Opt<T> 包装,需要报错
910
+ const typeAnnotation = member.typeAnnotation?.typeAnnotation;
911
+ const needsImport = !hasOptImport();
912
+
913
+ if (typeAnnotation) {
914
+ const currentTypeText = source.getText(typeAnnotation);
915
+ const wrappedType = wrapWithOpt(currentTypeText);
916
+
917
+ context.report({
918
+ node: member,
919
+ messageId: "useOptTypeForInitializedProperty",
920
+ fix: (fixer) => {
921
+ const fixes = [
922
+ fixer.replaceText(typeAnnotation, wrappedType),
923
+ ];
924
+
925
+ // 如果需要,添加 Opt 导入
926
+ if (needsImport) {
927
+ const importFix = addOptImport(fixer);
928
+ if (importFix) fixes.push(importFix);
929
+ }
930
+
931
+ return fixes;
932
+ },
933
+ });
934
+ } else if (member.value) {
935
+ // 没有类型注解,从初始化值推断类型
936
+ let inferredType: string | null = null;
937
+
938
+ if (member.value.type === AST_NODE_TYPES.Literal) {
939
+ const valueType = typeof member.value.value;
940
+ if (valueType === "boolean") inferredType = "boolean";
941
+ else if (valueType === "number") inferredType = "number";
942
+ else if (valueType === "string") inferredType = "string";
943
+ } else if (member.value.type === AST_NODE_TYPES.ArrayExpression) {
944
+ // 空数组 [] 的情况,需要从 @Property 装饰器推断类型
945
+ inferredType = "unknown[]";
946
+ } else if (member.value.type === AST_NODE_TYPES.NewExpression) {
947
+ // new Date() 的情况
948
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
949
+ if (member.value.callee?.type === AST_NODE_TYPES.Identifier) {
950
+ inferredType = member.value.callee.name;
951
+ }
952
+ }
953
+
954
+ if (inferredType) {
955
+ const wrappedType = wrapWithOpt(inferredType);
956
+ const propertyName = member.key;
957
+
958
+ context.report({
959
+ node: member,
960
+ messageId: "useOptTypeForInitializedProperty",
961
+ fix: (fixer) => {
962
+ const fixes = [
963
+ fixer.insertTextAfter(propertyName, `: ${wrappedType}`),
964
+ ];
965
+
966
+ // 如果需要,添加 Opt 导入
967
+ if (needsImport) {
968
+ const importFix = addOptImport(fixer);
969
+ if (importFix) fixes.push(importFix);
970
+ }
971
+
972
+ return fixes;
973
+ },
974
+ });
975
+ }
976
+ }
977
+ }
978
+
979
+ // 如果有 @Enum 装饰器,无论是否识别为枚举类型,都检查其配置
980
+ if (enumDecorator) {
981
+ const enumConfig = parseEnumDecorator(enumDecorator);
982
+
983
+ // 检查 nullable 配置是否与 TypeScript 类型匹配
984
+ if (enumConfig.nullable !== typeInfo.isNullable) {
985
+ const expectedEnumText = buildEnumDecorator(typeInfo);
986
+ context.report({
987
+ node: enumDecorator,
988
+ messageId: "alignPropertyDecoratorWithTsType",
989
+ fix: (fixer) => {
990
+ return fixer.replaceTextRange(
991
+ enumDecorator.range,
992
+ expectedEnumText,
993
+ );
994
+ },
995
+ });
996
+ }
997
+ // 有 @Enum 装饰器的属性,不需要 @Property 装饰器
998
+ return;
999
+ }
1000
+
1001
+ // 如果是枚举类型但没有 @Enum 装饰器
1002
+ if (typeInfo.isEnum) {
1003
+ // 如果有 @Property 装饰器,建议替换为 @Enum
1004
+ if (propertyDecorator) {
1005
+ context.report({
1006
+ node: propertyDecorator,
1007
+ messageId: "useEnumDecorator",
1008
+ fix: (fixer) => {
1009
+ const newDecoratorText = buildEnumDecorator(typeInfo);
1010
+ return fixer.replaceTextRange(
1011
+ propertyDecorator.range,
1012
+ newDecoratorText,
1013
+ );
1014
+ },
1015
+ });
1016
+ return;
1017
+ }
1018
+
1019
+ // 如果没有 @Enum 装饰器,添加它
1020
+ context.report({
1021
+ node: member,
1022
+ messageId: "useEnumDecorator",
1023
+ fix: (fixer) => {
1024
+ const newDecoratorText = buildEnumDecorator(typeInfo);
1025
+ return fixer.insertTextBeforeRange(
1026
+ member.range,
1027
+ newDecoratorText + "\n ",
1028
+ );
1029
+ },
1030
+ });
1031
+ return;
1032
+ }
1033
+
1034
+ // 非枚举类型:如果没有 @Property 装饰器,添加它
1035
+ if (!propertyDecorator) {
1036
+ context.report({
1037
+ node: member,
1038
+ messageId: "alignPropertyDecoratorWithTsType",
1039
+ fix: (fixer) => {
1040
+ const fixes = addPropertyDecorator(member, typeInfo);
1041
+ return applyFixes(fixer, fixes);
1042
+ },
1043
+ });
1044
+ return;
1045
+ }
1046
+
1047
+ // 检查现有 @Property 装饰器是否与类型匹配
1048
+ const currentConfig = parsePropertyDecorator(propertyDecorator);
1049
+ const expectedType = typeInfo.propertyType;
1050
+
1051
+ let needReport = false;
1052
+
1053
+ // 检查 type 配置
1054
+ if (expectedType && currentConfig.type !== expectedType) {
1055
+ // 对于 string 类型,接受多种有效配置
1056
+ if (
1057
+ expectedType === "t.string" &&
1058
+ isValidStringType(currentConfig.type)
1059
+ ) {
1060
+ // 当前配置是有效的 string 类型配置,不需要修改
1061
+ } else if (
1062
+ expectedType === "t.float" &&
1063
+ isValidNumberType(currentConfig.type)
1064
+ ) {
1065
+ // 当前配置是有效的 number 类型配置,不需要修改
1066
+ } else if (
1067
+ expectedType === "t.array" &&
1068
+ typeInfo.arrayElementTypeName === "number" &&
1069
+ isValidNumberArrayType(currentConfig.type)
1070
+ ) {
1071
+ // 当前配置是有效的 number[] 类型配置(t.array 或 VectorType),不需要修改
1072
+ } else if (expectedType === "t.json") {
1073
+ // 对于 t.json 类型(Record 或自定义类型数组)
1074
+ if (currentConfig.type === "t.json") {
1075
+ // t.json 配置正确,不需要修改
1076
+ } else {
1077
+ needReport = true;
1078
+ }
1079
+ } else {
1080
+ needReport = true;
1081
+ }
1082
+ } else if (!expectedType && currentConfig.type) {
1083
+ // 如果不需要 type 但当前有 type,也需要修正
1084
+ needReport = true;
1085
+ }
1086
+
1087
+ // 检查 nullable 配置
1088
+ if (currentConfig.nullable !== typeInfo.isNullable) {
1089
+ needReport = true;
1090
+ }
1091
+
1092
+ if (!needReport) return;
1093
+
1094
+ context.report({
1095
+ node: member,
1096
+ messageId: "alignPropertyDecoratorWithTsType",
1097
+ fix: (fixer) => {
1098
+ const fixes = fixWithTypeInfo(
1099
+ member,
1100
+ propertyDecorator,
1101
+ typeInfo,
1102
+ currentConfig,
1103
+ );
1104
+ return applyFixes(fixer, fixes);
1105
+ },
1106
+ });
1107
+ });
1108
+ },
1109
+ };
1110
+ },
1111
+ });