@lark-apaas/fullstack-presets 1.1.25 → 1.1.26-alpha.20261002143152

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.
@@ -11,4 +11,5 @@ export declare const customRules: {
11
11
  'no-welcome-index-route': import("eslint").Rule.RuleModule;
12
12
  'require-index-route': import("eslint").Rule.RuleModule;
13
13
  'no-duplicate-route-component': import("eslint").Rule.RuleModule;
14
+ 'validated-non-primitive-needs-element-type': import("eslint").Rule.RuleModule;
14
15
  };
@@ -16,6 +16,7 @@ const require_shared_request_type_1 = __importDefault(require("./require-shared-
16
16
  const no_welcome_index_route_1 = __importDefault(require("./no-welcome-index-route"));
17
17
  const require_index_route_1 = __importDefault(require("./require-index-route"));
18
18
  const no_duplicate_route_component_1 = __importDefault(require("./no-duplicate-route-component"));
19
+ const validated_non_primitive_needs_element_type_1 = __importDefault(require("./validated-non-primitive-needs-element-type"));
19
20
  exports.customRules = {
20
21
  'no-nested-styled-jsx': no_nested_styled_jsx_1.default,
21
22
  'no-styled-jsx-data-uri-url-ref': no_styled_jsx_data_uri_url_ref_1.default,
@@ -29,4 +30,5 @@ exports.customRules = {
29
30
  'no-welcome-index-route': no_welcome_index_route_1.default,
30
31
  'require-index-route': require_index_route_1.default,
31
32
  'no-duplicate-route-component': no_duplicate_route_component_1.default,
33
+ 'validated-non-primitive-needs-element-type': validated_non_primitive_needs_element_type_1.default,
32
34
  };
@@ -0,0 +1,3 @@
1
+ import type { Rule } from 'eslint';
2
+ declare const rule: Rule.RuleModule;
3
+ export default rule;
@@ -0,0 +1,330 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ const typescript_1 = __importDefault(require("typescript"));
7
+ const primitiveTypeNodes = new Set([
8
+ 'TSStringKeyword',
9
+ 'TSBooleanKeyword',
10
+ 'TSNumberKeyword',
11
+ 'TSLiteralType',
12
+ 'TSNullKeyword',
13
+ 'TSUnknownKeyword',
14
+ 'TSAnyKeyword',
15
+ 'TSObjectKeyword',
16
+ 'TSTypeLiteral',
17
+ ]);
18
+ const semanticPrimitiveFlags = typescript_1.default.TypeFlags.StringLike |
19
+ typescript_1.default.TypeFlags.NumberLike |
20
+ typescript_1.default.TypeFlags.BooleanLike |
21
+ typescript_1.default.TypeFlags.BigIntLike |
22
+ typescript_1.default.TypeFlags.ESSymbolLike |
23
+ typescript_1.default.TypeFlags.Null |
24
+ typescript_1.default.TypeFlags.Undefined |
25
+ typescript_1.default.TypeFlags.Void |
26
+ typescript_1.default.TypeFlags.Never |
27
+ typescript_1.default.TypeFlags.Any |
28
+ typescript_1.default.TypeFlags.Unknown;
29
+ const rule = {
30
+ meta: {
31
+ type: 'problem',
32
+ docs: {
33
+ description: 'Require validated non-primitive DTO properties to declare a usable class-transformer Type',
34
+ category: 'Possible Errors',
35
+ recommended: true,
36
+ },
37
+ schema: [
38
+ {
39
+ type: 'object',
40
+ additionalProperties: false,
41
+ properties: {
42
+ additionalTypeDecorators: {
43
+ type: 'array',
44
+ items: { type: 'string', minLength: 1 },
45
+ },
46
+ additionalCustomValidatorDecorators: {
47
+ type: 'array',
48
+ items: { type: 'string', minLength: 1 },
49
+ },
50
+ },
51
+ },
52
+ ],
53
+ messages: {
54
+ missingElementType: 'This validated object array needs an element transformer. Add @Type(() => Object) for ordinary objects, or introduce a runtime DTO class and use @Type(() => ChildDto). @IsObject({ each: true }) and @ValidateNested({ each: true }) validate elements but do not transform them.',
55
+ missingRuntimeElementType: 'This validated DTO array needs @Type(() => {{typeName}}) so class-transformer can construct each element. Add @ValidateNested({ each: true }) separately only when child fields must also be validated.',
56
+ missingAliasedRuntimeElementType: 'This validated DTO array needs @Type using the actual in-scope runtime DTO class value for its elements. Do not pass a TypeScript-only alias. Add @ValidateNested({ each: true }) separately only when child fields must also be validated.',
57
+ invalidElementType: 'The @Type decorator does not describe this ordinary object array. Use @Type(() => Object); TypeScript interfaces cannot be used as runtime constructors.',
58
+ invalidRuntimeElementType: 'The @Type decorator does not describe this nested DTO array. Use @Type(() => {{typeName}}) with the runtime DTO class.',
59
+ invalidAliasedRuntimeElementType: 'The @Type decorator does not describe this nested DTO array. Pass the actual in-scope runtime DTO class value, not a TypeScript-only alias.',
60
+ missingType: 'A validated non-primitive property needs a class-transformer @Type decorator with a runtime constructor. TypeScript interfaces cannot be used as runtime constructors.',
61
+ },
62
+ },
63
+ create(context) {
64
+ const parserServices = context.sourceCode.parserServices;
65
+ if (!parserServices?.program || !parserServices.esTreeNodeToTSNodeMap) {
66
+ throw new Error('@lark-apaas/validated-non-primitive-needs-element-type requires type information. Configure parserOptions.project for the server files.');
67
+ }
68
+ const checker = parserServices.program.getTypeChecker();
69
+ const nodeMap = parserServices.esTreeNodeToTSNodeMap;
70
+ const classValidatorDecoratorNames = new Set();
71
+ const options = (context.options[0] ?? {});
72
+ const customValidatorNames = new Set(options.additionalCustomValidatorDecorators ?? []);
73
+ const customTypeDecoratorNames = new Set(options.additionalTypeDecorators ?? []);
74
+ return {
75
+ // Match the upstream rule's import-aware class-validator detection.
76
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
77
+ ImportDeclaration(node) {
78
+ if (typeof node.source.value !== 'string' ||
79
+ !node.source.value.startsWith('class-validator')) {
80
+ return;
81
+ }
82
+ for (const specifier of node.specifiers) {
83
+ if (specifier.type === 'ImportSpecifier') {
84
+ classValidatorDecoratorNames.add(specifier.local.name);
85
+ }
86
+ }
87
+ },
88
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
89
+ PropertyDefinition(node) {
90
+ const annotation = node.typeAnnotation?.typeAnnotation;
91
+ if (!annotation)
92
+ return;
93
+ const validationDecorators = node.decorators.filter((decorator) => {
94
+ const name = getDecoratorName(decorator);
95
+ return (name !== null &&
96
+ (classValidatorDecoratorNames.has(name) ||
97
+ customValidatorNames.has(name)));
98
+ });
99
+ if (validationDecorators.length === 0)
100
+ return;
101
+ const validationNames = new Set(validationDecorators.map(getDecoratorName));
102
+ if (validationNames.has('IsEnum'))
103
+ return;
104
+ const annotationType = getSemanticType(annotation, nodeMap, checker);
105
+ const arrayElementTypes = annotationType
106
+ ? getArrayElementTypes(annotationType, checker)
107
+ : null;
108
+ const syntacticElementType = getSyntacticArrayElementType(annotation);
109
+ const isArray = arrayElementTypes !== null || syntacticElementType !== null;
110
+ if (isArray) {
111
+ if (arrayElementTypes?.every(elementType => isSemanticPrimitiveType(elementType, checker))) {
112
+ return;
113
+ }
114
+ if (arrayElementTypes?.every(elementType => isSemanticEnumType(elementType, checker))) {
115
+ return;
116
+ }
117
+ }
118
+ else {
119
+ if (isSyntacticPrimitiveType(annotation))
120
+ return;
121
+ if (annotationType && isSemanticEnumType(annotationType, checker)) {
122
+ return;
123
+ }
124
+ }
125
+ // A scalar plain object intentionally needs only runtime object validation.
126
+ // Semantic array resolution ensures aliases and nullable arrays never take this exemption.
127
+ if (!isArray && validationNames.has('IsObject'))
128
+ return;
129
+ const typeDecorators = node.decorators.filter((decorator) => {
130
+ const name = getDecoratorName(decorator);
131
+ return (name === 'Type' ||
132
+ (name !== null && customTypeDecoratorNames.has(name)));
133
+ });
134
+ if (!isArray) {
135
+ if (typeDecorators.length === 0) {
136
+ context.report({ node: node.key, messageId: 'missingType' });
137
+ }
138
+ return;
139
+ }
140
+ const runtimeConstructor = arrayElementTypes
141
+ ? getSharedRuntimeConstructor(arrayElementTypes, syntacticElementType, nodeMap, checker)
142
+ : null;
143
+ const expectedTypeName = runtimeConstructor?.name ?? 'Object';
144
+ const hasValidType = typeDecorators.some((decorator) => {
145
+ const name = getDecoratorName(decorator);
146
+ if (name !== 'Type')
147
+ return true;
148
+ return runtimeConstructor
149
+ ? typeDecoratorTargetsConstructor(decorator, runtimeConstructor.symbol, nodeMap, checker)
150
+ : getTypeDecoratorTargetName(decorator) === 'Object';
151
+ });
152
+ if (hasValidType)
153
+ return;
154
+ if (typeDecorators.length > 0) {
155
+ context.report({
156
+ node: node.key,
157
+ messageId: runtimeConstructor
158
+ ? runtimeConstructor.name
159
+ ? 'invalidRuntimeElementType'
160
+ : 'invalidAliasedRuntimeElementType'
161
+ : 'invalidElementType',
162
+ data: { typeName: expectedTypeName },
163
+ });
164
+ return;
165
+ }
166
+ context.report({
167
+ node: node.key,
168
+ messageId: runtimeConstructor
169
+ ? runtimeConstructor.name
170
+ ? 'missingRuntimeElementType'
171
+ : 'missingAliasedRuntimeElementType'
172
+ : 'missingElementType',
173
+ data: { typeName: expectedTypeName },
174
+ });
175
+ },
176
+ };
177
+ },
178
+ };
179
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
180
+ function getDecoratorName(decorator) {
181
+ const expression = decorator?.expression;
182
+ if (expression?.type !== 'CallExpression' ||
183
+ expression.callee?.type !== 'Identifier') {
184
+ return null;
185
+ }
186
+ return expression.callee.name;
187
+ }
188
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
189
+ function getTypeDecoratorTarget(decorator) {
190
+ const factory = decorator.expression?.arguments?.[0];
191
+ if (factory?.type !== 'ArrowFunctionExpression')
192
+ return null;
193
+ if (factory.body?.type !== 'BlockStatement')
194
+ return factory.body ?? null;
195
+ if (factory.body.body?.length === 1 &&
196
+ factory.body.body[0].type === 'ReturnStatement') {
197
+ return factory.body.body[0].argument ?? null;
198
+ }
199
+ return null;
200
+ }
201
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
202
+ function getTypeDecoratorTargetName(decorator) {
203
+ const target = getTypeDecoratorTarget(decorator);
204
+ return target?.type === 'Identifier' ? target.name : null;
205
+ }
206
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
207
+ function getSyntacticArrayElementType(annotation) {
208
+ if (annotation.type === 'TSArrayType')
209
+ return annotation.elementType;
210
+ if (annotation.type === 'TSTypeReference' &&
211
+ annotation.typeName?.type === 'Identifier' &&
212
+ (annotation.typeName.name === 'Array' ||
213
+ annotation.typeName.name === 'ReadonlyArray') &&
214
+ annotation.typeArguments?.params?.length === 1) {
215
+ return annotation.typeArguments.params[0];
216
+ }
217
+ return null;
218
+ }
219
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
220
+ function isSyntacticPrimitiveType(typeNode) {
221
+ if (primitiveTypeNodes.has(typeNode.type))
222
+ return true;
223
+ return (typeNode.type === 'TSUnionType' &&
224
+ typeNode.types.some((member) => primitiveTypeNodes.has(member.type)));
225
+ }
226
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
227
+ function getSemanticType(node, nodeMap, checker) {
228
+ const tsNode = nodeMap.get(node);
229
+ return tsNode ? checker.getTypeAtLocation(tsNode) : null;
230
+ }
231
+ function getArrayElementTypes(annotationType, checker) {
232
+ const nonNullable = checker.getNonNullableType(annotationType);
233
+ // Nullable arrays collapse to one non-nullable array. Other heterogeneous unions
234
+ // remain outside this rule because there is no single safe element transformer.
235
+ if (nonNullable.isUnion() || !checker.isArrayType(nonNullable))
236
+ return null;
237
+ const typeArguments = checker.getTypeArguments(nonNullable);
238
+ const elementType = typeArguments[0] ??
239
+ checker.getIndexTypeOfType(nonNullable, typescript_1.default.IndexKind.Number);
240
+ return elementType ? [elementType] : null;
241
+ }
242
+ function isSemanticPrimitiveType(type, checker) {
243
+ if ((type.flags & semanticPrimitiveFlags) !== 0)
244
+ return true;
245
+ const nonNullable = checker.getNonNullableType(type);
246
+ if (nonNullable.isUnion()) {
247
+ return nonNullable.types.every(member => isSemanticPrimitiveType(member, checker));
248
+ }
249
+ return (nonNullable.flags & semanticPrimitiveFlags) !== 0;
250
+ }
251
+ function isSemanticEnumType(type, checker) {
252
+ const nonNullable = checker.getNonNullableType(type);
253
+ if ((nonNullable.flags & typescript_1.default.TypeFlags.EnumLike) !== 0)
254
+ return true;
255
+ const symbol = nonNullable.aliasSymbol ?? nonNullable.getSymbol();
256
+ if (symbol?.declarations?.some(declaration => typescript_1.default.isEnumDeclaration(declaration))) {
257
+ return true;
258
+ }
259
+ return (nonNullable.isUnion() &&
260
+ nonNullable.types.length > 0 &&
261
+ nonNullable.types.every(member => member.symbol?.declarations?.some(declaration => typescript_1.default.isEnumMember(declaration) &&
262
+ typescript_1.default.isEnumDeclaration(declaration.parent))));
263
+ }
264
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
265
+ function getSharedRuntimeConstructor(elementTypes, syntacticElementType, nodeMap, checker) {
266
+ const candidates = elementTypes.flatMap(elementType => {
267
+ const nonNullable = checker.getNonNullableType(elementType);
268
+ return nonNullable.isUnion() ? nonNullable.types : [nonNullable];
269
+ });
270
+ if (candidates.length === 0)
271
+ return null;
272
+ const symbols = candidates.map(type => getRuntimeConstructorSymbol(type, checker));
273
+ const symbol = symbols[0];
274
+ if (!symbol || symbols.some(candidate => candidate !== symbol))
275
+ return null;
276
+ return {
277
+ symbol,
278
+ name: getUsableRuntimeConstructorName(syntacticElementType, symbol, nodeMap, checker),
279
+ };
280
+ }
281
+ function getRuntimeConstructorSymbol(instanceType, checker) {
282
+ const symbol = instanceType.getSymbol();
283
+ if (!symbol?.valueDeclaration)
284
+ return null;
285
+ const valueType = checker.getTypeOfSymbolAtLocation(symbol, symbol.valueDeclaration);
286
+ return valueType.getConstructSignatures().length > 0 ? symbol : null;
287
+ }
288
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
289
+ function getUsableRuntimeConstructorName(typeNode, expectedSymbol, nodeMap, checker) {
290
+ if (typeNode?.type !== 'TSTypeReference' ||
291
+ typeNode.typeName?.type !== 'Identifier') {
292
+ return null;
293
+ }
294
+ const tsTypeName = nodeMap.get(typeNode.typeName);
295
+ if (!tsTypeName)
296
+ return null;
297
+ const localSymbol = checker.getSymbolAtLocation(tsTypeName);
298
+ if (!localSymbol ||
299
+ isTypeOnlyImport(localSymbol) ||
300
+ resolveAlias(localSymbol, checker) !== expectedSymbol) {
301
+ return null;
302
+ }
303
+ return typeNode.typeName.name;
304
+ }
305
+ function isTypeOnlyImport(symbol) {
306
+ return (symbol.declarations?.some(declaration => {
307
+ if (typescript_1.default.isImportSpecifier(declaration)) {
308
+ return declaration.isTypeOnly || declaration.parent.parent.isTypeOnly;
309
+ }
310
+ return typescript_1.default.isImportClause(declaration) && declaration.isTypeOnly;
311
+ }) ?? false);
312
+ }
313
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
314
+ function typeDecoratorTargetsConstructor(decorator, expectedSymbol, nodeMap, checker) {
315
+ const target = getTypeDecoratorTarget(decorator);
316
+ const tsTarget = target ? nodeMap.get(target) : undefined;
317
+ if (!tsTarget)
318
+ return false;
319
+ const localSymbol = checker.getSymbolAtLocation(tsTarget);
320
+ return (localSymbol !== undefined &&
321
+ !isTypeOnlyImport(localSymbol) &&
322
+ resolveAlias(localSymbol, checker) === expectedSymbol);
323
+ }
324
+ function resolveAlias(symbol, checker) {
325
+ if (symbol && (symbol.flags & typescript_1.default.SymbolFlags.Alias) !== 0) {
326
+ return checker.getAliasedSymbol(symbol);
327
+ }
328
+ return symbol;
329
+ }
330
+ exports.default = rule;
@@ -27,6 +27,7 @@ declare const _default: {
27
27
  'no-welcome-index-route': import("eslint").Rule.RuleModule;
28
28
  'require-index-route': import("eslint").Rule.RuleModule;
29
29
  'no-duplicate-route-component': import("eslint").Rule.RuleModule;
30
+ 'validated-non-primitive-needs-element-type': import("eslint").Rule.RuleModule;
30
31
  };
31
32
  };
32
33
  };
@@ -30,6 +30,7 @@ export declare const eslintPresets: {
30
30
  'no-welcome-index-route': import("eslint").Rule.RuleModule;
31
31
  'require-index-route': import("eslint").Rule.RuleModule;
32
32
  'no-duplicate-route-component': import("eslint").Rule.RuleModule;
33
+ 'validated-non-primitive-needs-element-type': import("eslint").Rule.RuleModule;
33
34
  };
34
35
  };
35
36
  };
@@ -48,6 +48,8 @@ declare const _default: {
48
48
  '@darraghor/nestjs-typed/api-property-returning-array-should-set-array': string;
49
49
  '@darraghor/nestjs-typed/api-property-matches-property-optionality': string;
50
50
  '@darraghor/nestjs-typed/all-properties-have-explicit-defined': string;
51
+ '@darraghor/nestjs-typed/validated-non-primitive-property-needs-type-decorator': string;
52
+ '@lark-apaas/validated-non-primitive-needs-element-type': string;
51
53
  '@darraghor/nestjs-typed/controllers-should-supply-api-tags': string;
52
54
  '@darraghor/nestjs-typed/api-method-should-specify-api-response': string;
53
55
  '@darraghor/nestjs-typed/api-method-should-specify-api-operation': string;
@@ -69,6 +69,9 @@ exports.default = {
69
69
  '@darraghor/nestjs-typed/api-property-returning-array-should-set-array': 'off',
70
70
  '@darraghor/nestjs-typed/api-property-matches-property-optionality': 'off',
71
71
  '@darraghor/nestjs-typed/all-properties-have-explicit-defined': 'off',
72
+ // 本地兼容规则保留上游的非原始类型检查,并修复对象数组的 IsObject(each) 豁免与 interface 建议。
73
+ '@darraghor/nestjs-typed/validated-non-primitive-property-needs-type-decorator': 'off',
74
+ '@lark-apaas/validated-non-primitive-needs-element-type': 'error',
72
75
  // 关闭 Swagger 相关规则
73
76
  '@darraghor/nestjs-typed/controllers-should-supply-api-tags': 'off',
74
77
  '@darraghor/nestjs-typed/api-method-should-specify-api-response': 'off',
@@ -57,6 +57,7 @@ export declare const eslintPresets: {
57
57
  'no-welcome-index-route': import("eslint").Rule.RuleModule;
58
58
  'require-index-route': import("eslint").Rule.RuleModule;
59
59
  'no-duplicate-route-component': import("eslint").Rule.RuleModule;
60
+ 'validated-non-primitive-needs-element-type': import("eslint").Rule.RuleModule;
60
61
  };
61
62
  };
62
63
  };
@@ -82,6 +83,7 @@ export declare const eslintPresets: {
82
83
  'no-welcome-index-route': import("eslint").Rule.RuleModule;
83
84
  'require-index-route': import("eslint").Rule.RuleModule;
84
85
  'no-duplicate-route-component': import("eslint").Rule.RuleModule;
86
+ 'validated-non-primitive-needs-element-type': import("eslint").Rule.RuleModule;
85
87
  };
86
88
  };
87
89
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lark-apaas/fullstack-presets",
3
- "version": "1.1.25",
3
+ "version": "1.1.26-alpha.20261002143152",
4
4
  "files": [
5
5
  "lib"
6
6
  ],