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