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

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 +2 -20
  3. package/dist/index.d.ts +2 -0
  4. package/dist/index.js +2 -5
  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 +38 -2
  41. package/jest.config.ts +12 -0
  42. package/package.json +22 -14
  43. package/src/index.ts +1 -1
  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,557 @@
1
+ import { AST_NODE_TYPES, TSESTree } from "@typescript-eslint/utils";
2
+ import type { RuleFixer } from "@typescript-eslint/utils/ts-eslint";
3
+
4
+ import { createRule } from "../../utils/createRule";
5
+ import {
6
+ getPropertyDecorator,
7
+ hasClassDecorator,
8
+ } from "../../utils/decorators";
9
+
10
+ // 自定义 Fix 对象类型,用于延迟应用修复
11
+ interface CustomFix {
12
+ type: "insert" | "replace";
13
+ range: readonly [number, number];
14
+ text: string;
15
+ }
16
+
17
+ interface TypeInfo {
18
+ typeName: string | null;
19
+ isArray: boolean;
20
+ isNullable: boolean;
21
+ }
22
+
23
+ export type DecoratorBehavior = "ignore" | "remove";
24
+
25
+ export interface Options {
26
+ decorators?: Record<string, DecoratorBehavior>;
27
+ }
28
+
29
+ export default createRule<
30
+ [Options],
31
+ "alignFieldDecoratorWithTsType" | "removeFieldDecorator"
32
+ >({
33
+ name: "graphql-field-config-from-types",
34
+ meta: {
35
+ type: "problem",
36
+ docs: {
37
+ description:
38
+ "根据 TypeScript 类型自动生成或修正 @Field 装饰器的类型与 nullable 配置(支持数组)。",
39
+ },
40
+ fixable: "code",
41
+ schema: [
42
+ {
43
+ type: "object",
44
+ properties: {
45
+ decorators: {
46
+ type: "object",
47
+ additionalProperties: {
48
+ type: "string",
49
+ enum: ["ignore", "remove"],
50
+ },
51
+ description:
52
+ "配置每个装饰器的行为。ignore: 跳过检查;remove: 移除 @Field。默认:{ HideField: 'remove', OneToOne: 'remove', OneToMany: 'remove', ManyToOne: 'remove', ManyToMany: 'remove' }",
53
+ },
54
+ },
55
+ additionalProperties: false,
56
+ },
57
+ ],
58
+ messages: {
59
+ alignFieldDecoratorWithTsType:
60
+ "@Field 装饰器应与 TypeScript 类型保持一致(类型与 nullable)。",
61
+ removeFieldDecorator:
62
+ "属性带有 @{{decoratorName}} 装饰器,应移除 @Field 装饰器。",
63
+ },
64
+ },
65
+ defaultOptions: [
66
+ {
67
+ decorators: {
68
+ HideField: "remove",
69
+ OneToOne: "remove",
70
+ OneToMany: "remove",
71
+ ManyToOne: "remove",
72
+ ManyToMany: "remove",
73
+ },
74
+ },
75
+ ],
76
+ create(context, [options]) {
77
+ const source = context.sourceCode;
78
+
79
+ const scalarFromTsKeyword = (
80
+ typeNodeType: AST_NODE_TYPES,
81
+ ): string | null => {
82
+ switch (typeNodeType) {
83
+ case AST_NODE_TYPES.TSBooleanKeyword:
84
+ return "Boolean";
85
+ case AST_NODE_TYPES.TSStringKeyword:
86
+ return "String";
87
+ case AST_NODE_TYPES.TSNumberKeyword:
88
+ // 默认 number → Float(GraphQL 默认也是 Float;Int 需显式声明)
89
+ return "Float";
90
+ default:
91
+ return null;
92
+ }
93
+ };
94
+
95
+ const getIdentifierName = (node: TSESTree.TypeNode): string | null => {
96
+ if (
97
+ node.type === AST_NODE_TYPES.TSTypeReference &&
98
+ node.typeName.type === AST_NODE_TYPES.Identifier
99
+ ) {
100
+ return node.typeName.name;
101
+ }
102
+ return null;
103
+ };
104
+
105
+ const extractArrayElementType = (
106
+ node: TSESTree.TypeNode,
107
+ ): TSESTree.TypeNode | null => {
108
+ if (node.type === AST_NODE_TYPES.TSArrayType) {
109
+ return node.elementType;
110
+ }
111
+ if (
112
+ node.type === AST_NODE_TYPES.TSTypeReference &&
113
+ node.typeName.type === AST_NODE_TYPES.Identifier &&
114
+ node.typeName.name === "Array"
115
+ ) {
116
+ return node.typeArguments?.params[0] ?? null;
117
+ }
118
+ return null;
119
+ };
120
+
121
+ const computeTypeInfo = (
122
+ property: TSESTree.PropertyDefinition,
123
+ ): TypeInfo | null => {
124
+ let isNullable = false;
125
+ let isArray = false;
126
+
127
+ let baseTypeNode: TSESTree.TypeNode | null =
128
+ property.typeAnnotation?.type === AST_NODE_TYPES.TSTypeAnnotation
129
+ ? property.typeAnnotation.typeAnnotation
130
+ : null;
131
+
132
+ // 可选属性(?)视为可空
133
+ if (property.optional) {
134
+ isNullable = true;
135
+ }
136
+
137
+ // 处理联合类型中的 null/undefined
138
+ if (baseTypeNode?.type === AST_NODE_TYPES.TSUnionType) {
139
+ const hasNullish = baseTypeNode.types.some((t: TSESTree.TypeNode) => {
140
+ return (
141
+ t.type === AST_NODE_TYPES.TSNullKeyword ||
142
+ t.type === AST_NODE_TYPES.TSUndefinedKeyword
143
+ );
144
+ });
145
+ if (hasNullish) isNullable = true;
146
+
147
+ baseTypeNode =
148
+ baseTypeNode.types.find((t: TSESTree.TypeNode) => {
149
+ return (
150
+ t.type !== AST_NODE_TYPES.TSNullKeyword &&
151
+ t.type !== AST_NODE_TYPES.TSUndefinedKeyword
152
+ );
153
+ }) ?? null;
154
+ }
155
+
156
+ // 先解包 Ref<T> 和 Opt<T> → T,并在内部再次处理 null/undefined
157
+ if (
158
+ baseTypeNode?.type === AST_NODE_TYPES.TSTypeReference &&
159
+ baseTypeNode.typeName.type === AST_NODE_TYPES.Identifier &&
160
+ (baseTypeNode.typeName.name === "Ref" ||
161
+ baseTypeNode.typeName.name === "Opt")
162
+ ) {
163
+ let inner = baseTypeNode.typeArguments?.params[0] ?? null;
164
+ if (inner && inner.type === AST_NODE_TYPES.TSUnionType) {
165
+ const hasNullish = inner.types.some((t: TSESTree.TypeNode) => {
166
+ return (
167
+ t.type === AST_NODE_TYPES.TSNullKeyword ||
168
+ t.type === AST_NODE_TYPES.TSUndefinedKeyword
169
+ );
170
+ });
171
+ if (hasNullish) isNullable = true;
172
+ inner =
173
+ inner.types.find((t: TSESTree.TypeNode) => {
174
+ return (
175
+ t.type !== AST_NODE_TYPES.TSNullKeyword &&
176
+ t.type !== AST_NODE_TYPES.TSUndefinedKeyword
177
+ );
178
+ }) ?? inner;
179
+ }
180
+ baseTypeNode = inner ?? baseTypeNode;
181
+ }
182
+
183
+ // 数组类型(T[] 或 Array<T>)- 在解包 Opt/Ref 之后检查
184
+ const elementTypeNode = baseTypeNode
185
+ ? extractArrayElementType(baseTypeNode)
186
+ : null;
187
+ if (elementTypeNode) {
188
+ isArray = true;
189
+ }
190
+
191
+ const targetTypeNode: TSESTree.TypeNode | null =
192
+ elementTypeNode ?? baseTypeNode;
193
+
194
+ // 无显式类型时,尝试从字面量初始值推断
195
+ if (!targetTypeNode) {
196
+ if (property.value?.type === AST_NODE_TYPES.Literal) {
197
+ const value = property.value.value;
198
+ const typeOf = typeof value;
199
+ if (typeOf === "boolean")
200
+ return { typeName: "Boolean", isArray, isNullable };
201
+ if (typeOf === "number")
202
+ return { typeName: "Float", isArray, isNullable };
203
+ if (typeOf === "string")
204
+ return { typeName: "String", isArray, isNullable };
205
+ }
206
+ return null;
207
+ }
208
+
209
+ // id 字段优先使用 GraphQL 标量 ID
210
+ const propertyName =
211
+ property.key.type === AST_NODE_TYPES.Identifier
212
+ ? property.key.name
213
+ : null;
214
+
215
+ if (
216
+ propertyName === "id" ||
217
+ (typeof propertyName === "string" &&
218
+ (propertyName.endsWith("Id") || propertyName.endsWith("ID")))
219
+ ) {
220
+ return { typeName: "ID", isArray, isNullable };
221
+ }
222
+
223
+ // Record<*, *> → GraphQLJSONObject(不限制键值类型)
224
+ if (
225
+ targetTypeNode.type === AST_NODE_TYPES.TSTypeReference &&
226
+ targetTypeNode.typeName.type === AST_NODE_TYPES.Identifier &&
227
+ targetTypeNode.typeName.name === "Record"
228
+ ) {
229
+ return { typeName: "GraphQLJSONObject", isArray, isNullable };
230
+ }
231
+
232
+ // 关键字类型 → GraphQL 标量
233
+ const scalar = scalarFromTsKeyword(targetTypeNode.type);
234
+ if (scalar) {
235
+ return { typeName: scalar, isArray, isNullable };
236
+ }
237
+
238
+ // 标识符(类/自定义类型)
239
+ const ident = getIdentifierName(targetTypeNode);
240
+ if (ident) {
241
+ return { typeName: ident, isArray, isNullable };
242
+ }
243
+
244
+ return null;
245
+ };
246
+
247
+ const ensureScalarImport = (fixes: CustomFix[], expected: string) => {
248
+ if (["Int", "Float", "ID"].includes(expected)) {
249
+ if (
250
+ !context.sourceCode.text.includes(
251
+ `import { ${expected} } from "@nestjs/graphql"`,
252
+ )
253
+ ) {
254
+ fixes.push({
255
+ type: "insert",
256
+ range: [0, 0],
257
+ text: `import { ${expected} } from "@nestjs/graphql";\n`,
258
+ });
259
+ }
260
+ }
261
+ };
262
+
263
+ const ensureJSONObjectImport = (fixes: CustomFix[]) => {
264
+ const hasImport =
265
+ context.sourceCode.text.includes(`from "graphql-type-json"`) &&
266
+ context.sourceCode.text.includes("GraphQLJSONObject");
267
+ if (!hasImport) {
268
+ fixes.push({
269
+ type: "insert",
270
+ range: [0, 0],
271
+ text: `import { GraphQLJSONObject } from "graphql-type-json";\n`,
272
+ });
273
+ }
274
+ };
275
+
276
+ const addFieldDecorator = (
277
+ property: TSESTree.PropertyDefinition,
278
+ info: TypeInfo,
279
+ ) => {
280
+ const fixes: CustomFix[] = [];
281
+
282
+ const typeName = info.typeName ?? "";
283
+ const typeExpr = info.isArray
284
+ ? `() => [${typeName}]`
285
+ : `() => ${typeName}`;
286
+ const optionsExpr = info.isNullable ? ", { nullable: true }" : "";
287
+
288
+ const newDecoratorText = `@Field(${typeExpr}${optionsExpr})`;
289
+
290
+ ensureScalarImport(fixes, info.typeName ?? "");
291
+ if (info.typeName === "GraphQLJSONObject") {
292
+ ensureJSONObjectImport(fixes);
293
+ }
294
+
295
+ fixes.push({
296
+ type: "insert",
297
+ range: property.range,
298
+ text: newDecoratorText + "\n ",
299
+ });
300
+
301
+ return fixes;
302
+ };
303
+
304
+ const fixWithTypeInfo = (
305
+ property: TSESTree.PropertyDefinition,
306
+ fieldDecorator: TSESTree.Decorator,
307
+ info: TypeInfo,
308
+ ) => {
309
+ const fixes: CustomFix[] = [];
310
+
311
+ const typeName = info.typeName ?? "";
312
+ const typeExpr = info.isArray
313
+ ? `() => [${typeName}]`
314
+ : `() => ${typeName}`;
315
+
316
+ // 提取现有的配置选项(除了 nullable)
317
+ const callExpr = fieldDecorator.expression;
318
+ const existingOptions: string[] = [];
319
+
320
+ // 检查是否有现有的配置对象
321
+ let optionsArg: TSESTree.ObjectExpression | null = null;
322
+ if (
323
+ callExpr.type === AST_NODE_TYPES.CallExpression &&
324
+ callExpr.arguments.length > 0
325
+ ) {
326
+ // 如果第一个参数是对象表达式(没有 type function)
327
+ if (
328
+ callExpr.arguments[0] &&
329
+ callExpr.arguments[0].type === AST_NODE_TYPES.ObjectExpression
330
+ ) {
331
+ optionsArg = callExpr.arguments[0];
332
+ }
333
+ // 如果第二个参数是对象表达式(有 type function)
334
+ else if (
335
+ callExpr.arguments[1] &&
336
+ callExpr.arguments[1].type === AST_NODE_TYPES.ObjectExpression
337
+ ) {
338
+ optionsArg = callExpr.arguments[1];
339
+ }
340
+ }
341
+
342
+ // 提取现有选项,同时处理 nullable
343
+ let hasNullableProperty = false;
344
+ if (optionsArg) {
345
+ optionsArg.properties.forEach((prop: TSESTree.ObjectLiteralElement) => {
346
+ // 处理展开运算符 (SpreadElement)
347
+ if (prop.type === AST_NODE_TYPES.SpreadElement) {
348
+ const propText = source.getText(prop);
349
+ existingOptions.push(propText);
350
+ return;
351
+ }
352
+ // 处理普通属性 (Property)
353
+ if (
354
+ prop.key.type === AST_NODE_TYPES.Identifier &&
355
+ prop.key.name === "nullable"
356
+ ) {
357
+ hasNullableProperty = true;
358
+ // 根据类型决定是否保留或更新 nullable
359
+ if (info.isNullable) {
360
+ existingOptions.push("nullable: true");
361
+ }
362
+ // 如果不需要 nullable,则跳过(不保留)
363
+ } else {
364
+ // 保留原有的其他配置
365
+ const propText = source.getText(prop);
366
+ existingOptions.push(propText);
367
+ }
368
+ });
369
+ }
370
+
371
+ // 如果需要 nullable 但原配置中没有,则添加到最前面
372
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
373
+ if (info.isNullable && !hasNullableProperty) {
374
+ existingOptions.unshift("nullable: true");
375
+ }
376
+
377
+ // 构建新的装饰器文本
378
+ const optionsExpr =
379
+ existingOptions.length > 0 ? `, { ${existingOptions.join(", ")} }` : "";
380
+ const newDecoratorText = `@Field(${typeExpr}${optionsExpr})`;
381
+
382
+ ensureScalarImport(fixes, info.typeName ?? "");
383
+ if (info.typeName === "GraphQLJSONObject") {
384
+ ensureJSONObjectImport(fixes);
385
+ }
386
+
387
+ fixes.push({
388
+ type: "replace",
389
+ range: fieldDecorator.range,
390
+ text: newDecoratorText,
391
+ });
392
+
393
+ return fixes;
394
+ };
395
+
396
+ const applyFixes = (fixer: RuleFixer, fixes: CustomFix[]) => {
397
+ return fixes.map((fix) => {
398
+ if (fix.type === "replace") {
399
+ return fixer.replaceTextRange(fix.range, fix.text);
400
+ } else {
401
+ return fixer.insertTextBeforeRange(fix.range, fix.text);
402
+ }
403
+ });
404
+ };
405
+
406
+ const isGraphqlModelClass = (node: TSESTree.ClassDeclaration): boolean => {
407
+ return hasClassDecorator(node, ["ObjectType", "InputType", "ArgsType"]);
408
+ };
409
+
410
+ return {
411
+ ClassDeclaration(node) {
412
+ if (!isGraphqlModelClass(node)) return;
413
+
414
+ node.body.body.forEach((member: TSESTree.ClassElement) => {
415
+ if (member.type !== AST_NODE_TYPES.PropertyDefinition) return;
416
+
417
+ const decoratorConfig = options.decorators ?? {
418
+ HideField: "remove",
419
+ OneToOne: "remove",
420
+ OneToMany: "remove",
421
+ ManyToOne: "remove",
422
+ ManyToMany: "remove",
423
+ };
424
+
425
+ // 检查是否有配置的装饰器
426
+ let foundDecoratorName: string | null = null;
427
+ let foundBehavior: DecoratorBehavior | null = null;
428
+
429
+ for (const decorator of member.decorators) {
430
+ if (
431
+ decorator.expression.type === AST_NODE_TYPES.CallExpression &&
432
+ decorator.expression.callee.type === AST_NODE_TYPES.Identifier
433
+ ) {
434
+ const decoratorName = decorator.expression.callee.name;
435
+ if (decoratorName in decoratorConfig) {
436
+ foundDecoratorName = decoratorName;
437
+ foundBehavior = decoratorConfig[decoratorName];
438
+ break;
439
+ }
440
+ }
441
+ }
442
+
443
+ // 如果是 ignore 行为,直接跳过
444
+ if (foundBehavior === "ignore") return;
445
+
446
+ const fieldDecorator = getPropertyDecorator(member, "Field");
447
+
448
+ // 如果是 remove 行为且有 @Field,则移除 @Field
449
+ if (foundBehavior === "remove" && fieldDecorator) {
450
+ context.report({
451
+ node: fieldDecorator,
452
+ messageId: "removeFieldDecorator",
453
+ data: {
454
+ decoratorName: foundDecoratorName ?? "unknown",
455
+ },
456
+ fix: (fixer) => {
457
+ // 移除整个装饰器行(包括换行)
458
+ const decoratorStart = fieldDecorator.range[0];
459
+ const decoratorEnd = fieldDecorator.range[1];
460
+
461
+ // 查找装饰器后的换行符和空格
462
+ const textAfter = source.text.slice(
463
+ decoratorEnd,
464
+ decoratorEnd + 10,
465
+ );
466
+ const matchNewline = /^(\r?\n\s*)/.exec(textAfter);
467
+ const endPos = matchNewline
468
+ ? decoratorEnd + matchNewline[0].length
469
+ : decoratorEnd;
470
+
471
+ return fixer.removeRange([decoratorStart, endPos]);
472
+ },
473
+ });
474
+ return;
475
+ }
476
+
477
+ // 如果是 remove 行为但没有 @Field,跳过
478
+ if (foundBehavior === "remove") return;
479
+
480
+ const typeInfo = computeTypeInfo(member);
481
+ if (!typeInfo?.typeName) return;
482
+
483
+ // 如果没有 @Field 装饰器,添加它
484
+ if (!fieldDecorator) {
485
+ context.report({
486
+ node: member,
487
+ messageId: "alignFieldDecoratorWithTsType",
488
+ fix: (fixer) => {
489
+ const fixes = addFieldDecorator(member, typeInfo);
490
+ return applyFixes(fixer, fixes);
491
+ },
492
+ });
493
+ return;
494
+ }
495
+
496
+ // 若已有 ArrowFunction 但与期望不一致,也进行修正
497
+ const callExpr = fieldDecorator.expression;
498
+ if (callExpr.type !== AST_NODE_TYPES.CallExpression) return;
499
+
500
+ let needReport = true;
501
+
502
+ if (
503
+ callExpr.arguments.length > 0 &&
504
+ callExpr.arguments[0].type ===
505
+ AST_NODE_TYPES.ArrowFunctionExpression
506
+ ) {
507
+ const firstArg = callExpr.arguments[0];
508
+ const calleeText = source.getText(firstArg.body);
509
+ const expectedTypeText = typeInfo.isArray
510
+ ? `[${typeInfo.typeName}]`
511
+ : typeInfo.typeName;
512
+
513
+ const hasNullableOption =
514
+ callExpr.arguments.length > 1 &&
515
+ callExpr.arguments[1].type === AST_NODE_TYPES.ObjectExpression &&
516
+ callExpr.arguments[1].properties.some(
517
+ (prop: TSESTree.ObjectLiteralElement) => {
518
+ return (
519
+ prop.type === AST_NODE_TYPES.Property &&
520
+ prop.key.type === AST_NODE_TYPES.Identifier &&
521
+ prop.key.name === "nullable" &&
522
+ prop.value.type === AST_NODE_TYPES.Literal &&
523
+ prop.value.value === true
524
+ );
525
+ },
526
+ );
527
+
528
+ // 对于 number 类型,Int 和 Float 都是合法的
529
+ const isNumberType = typeInfo.typeName === "Float";
530
+ const actualTypeText = calleeText.replace(/^\[|\]$/g, ""); // 移除数组括号
531
+ const isValidNumberType =
532
+ isNumberType &&
533
+ (actualTypeText === "Int" || actualTypeText === "Float");
534
+
535
+ const typeMatches =
536
+ calleeText === expectedTypeText || isValidNumberType;
537
+
538
+ if (typeMatches && hasNullableOption === typeInfo.isNullable) {
539
+ needReport = false;
540
+ }
541
+ }
542
+
543
+ if (!needReport) return;
544
+
545
+ context.report({
546
+ node: member,
547
+ messageId: "alignFieldDecoratorWithTsType",
548
+ fix: (fixer) => {
549
+ const fixes = fixWithTypeInfo(member, fieldDecorator, typeInfo);
550
+ return applyFixes(fixer, fixes);
551
+ },
552
+ });
553
+ });
554
+ },
555
+ };
556
+ },
557
+ });
@@ -0,0 +1,135 @@
1
+ import { tester } from "../../utils/tester";
2
+ import rule from "./graphql-field-definite-assignment";
3
+
4
+ tester.run("graphql-field-definite-assignment", rule, {
5
+ valid: [
6
+ // 有初始化值的属性,不需要 !
7
+ /* typescript */ `
8
+ @ObjectType()
9
+ class User {
10
+ @Field()
11
+ createdAt: Date = new Date();
12
+ }
13
+ `,
14
+ // 有 ! 的属性,没有初始化值
15
+ /* typescript */ `
16
+ @ObjectType()
17
+ class User {
18
+ @Field()
19
+ name!: string;
20
+ }
21
+ `,
22
+ // 可选属性,不需要 !
23
+ /* typescript */ `
24
+ @ObjectType()
25
+ class User {
26
+ @Field()
27
+ age?: number;
28
+ }
29
+ `,
30
+ // InputType 类型
31
+ /* typescript */ `
32
+ @InputType()
33
+ class CreateUserInput {
34
+ @Field()
35
+ name!: string;
36
+ }
37
+ `,
38
+ // ArgsType 类型
39
+ /* typescript */ `
40
+ @ArgsType()
41
+ class GetUserArgs {
42
+ @Field()
43
+ id!: string;
44
+ }
45
+ `,
46
+ // 非 GraphQL 模型类不检查
47
+ /* typescript */ `
48
+ class NotAGraphQLModel {
49
+ @Field()
50
+ field: string;
51
+ }
52
+ `,
53
+ // 没有 @Field 装饰器不检查
54
+ /* typescript */ `
55
+ @ObjectType()
56
+ class User {
57
+ field: string;
58
+ }
59
+ `,
60
+ ],
61
+ invalid: [
62
+ // 没有初始化值,也没有 !
63
+ {
64
+ code: /* typescript */ `
65
+ @ObjectType()
66
+ class User {
67
+ @Field()
68
+ name: string;
69
+ }
70
+ `,
71
+ output: /* typescript */ `
72
+ @ObjectType()
73
+ class User {
74
+ @Field()
75
+ name!: string;
76
+ }
77
+ `,
78
+ errors: [{ messageId: "addDefiniteAssignment" }],
79
+ },
80
+ // 有初始化值,但也有 !
81
+ {
82
+ code: /* typescript */ `
83
+ @ObjectType()
84
+ class User {
85
+ @Field()
86
+ createdAt!: Date = new Date();
87
+ }
88
+ `,
89
+ output: /* typescript */ `
90
+ @ObjectType()
91
+ class User {
92
+ @Field()
93
+ createdAt: Date = new Date();
94
+ }
95
+ `,
96
+ errors: [{ messageId: "removeDefiniteAssignment" }],
97
+ },
98
+ // InputType - 没有初始化值,也没有 !
99
+ {
100
+ code: /* typescript */ `
101
+ @InputType()
102
+ class CreateUserInput {
103
+ @Field()
104
+ name: string;
105
+ }
106
+ `,
107
+ output: /* typescript */ `
108
+ @InputType()
109
+ class CreateUserInput {
110
+ @Field()
111
+ name!: string;
112
+ }
113
+ `,
114
+ errors: [{ messageId: "addDefiniteAssignment" }],
115
+ },
116
+ // 数字类型,没有初始化值和 !
117
+ {
118
+ code: /* typescript */ `
119
+ @ObjectType()
120
+ class User {
121
+ @Field()
122
+ age: number;
123
+ }
124
+ `,
125
+ output: /* typescript */ `
126
+ @ObjectType()
127
+ class User {
128
+ @Field()
129
+ age!: number;
130
+ }
131
+ `,
132
+ errors: [{ messageId: "addDefiniteAssignment" }],
133
+ },
134
+ ],
135
+ });