@webergency-utils/typechecker 0.1.0

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.
@@ -0,0 +1,882 @@
1
+ import ts from 'typescript';
2
+ import { createPrimitiveCheck, createLiteralCheck, createArrayCheck, createUnionCheck, createObjectCheck, createDateCheck, createNullCheck, createUndefinedCheck, createIntersectionCheck, createTupleCheck, createRecordCheck, createRegExpCheck, createTemplateLiteralCheck, createConstrainedPrimitiveCheck, createSetCheck, createMapCheck } from './generators.js';
3
+ import { createHash } from 'crypto';
4
+ function getStringLiteralValue(type) {
5
+ if (type.isStringLiteral()) {
6
+ return type.value;
7
+ }
8
+ if (type.isUnion()) {
9
+ const literalType = type.types.find(t => t.isStringLiteral());
10
+ if (literalType && literalType.isStringLiteral()) {
11
+ return literalType.value;
12
+ }
13
+ }
14
+ return undefined;
15
+ }
16
+ function minifyTypeString(str) {
17
+ return str
18
+ .replace(/\{\s+/g, '{')
19
+ .replace(/\s+\}/g, '}')
20
+ .replace(/;\s*\}/g, '}')
21
+ .replace(/;\s+/g, ',')
22
+ .replace(/:\s+/g, ':')
23
+ .replace(/\s+\|\s+/g, '|');
24
+ }
25
+ export function buildValidator(type, checker, validatorsMap, requiredUtils) {
26
+ const hash = generateHash(type, checker);
27
+ if (validatorsMap.has(hash)) {
28
+ return ts.factory.createIdentifier(`__val_${hash}`);
29
+ }
30
+ // Set placeholder to handle circularity
31
+ validatorsMap.set(hash, ts.factory.createIdentifier(`PENDING_${hash}`));
32
+ let result;
33
+ const flags = type.getFlags();
34
+ const isUnion = (((flags & ts.TypeFlags.Union) !== 0 || type.isUnion()) && type.types) ? true : false;
35
+ const isIntersection = (((flags & ts.TypeFlags.Intersection) !== 0 || type.isIntersection()) && type.types) ? true : false;
36
+ if (isUnion) {
37
+ const checks = type.types.map(t => buildValidator(t, checker, validatorsMap, requiredUtils));
38
+ result = createUnionCheck(checks, requiredUtils, `Type<${minifyTypeString(checker.typeToString(type))}>`);
39
+ }
40
+ else if (isIntersection) {
41
+ const types = type.types;
42
+ let baseName = '';
43
+ let baseType;
44
+ const constraints = [];
45
+ for (const sub of types) {
46
+ const sFlags = sub.getFlags();
47
+ if (sFlags & ts.TypeFlags.String || sFlags & ts.TypeFlags.TemplateLiteral) {
48
+ baseName = 'string';
49
+ baseType = sub;
50
+ }
51
+ else if (sFlags & ts.TypeFlags.Number) {
52
+ baseName = 'number';
53
+ }
54
+ else if (sFlags & ts.TypeFlags.BigInt) {
55
+ baseName = 'bigint';
56
+ }
57
+ else if (sFlags & ts.TypeFlags.Boolean || sub.intrinsicName === 'boolean') {
58
+ baseName = 'boolean';
59
+ baseType = sub;
60
+ }
61
+ else if (sub.getSymbol()?.name === 'Date') {
62
+ baseName = 'date';
63
+ baseType = sub;
64
+ }
65
+ else if (checker.isArrayType(sub)) {
66
+ baseName = 'array';
67
+ baseType = sub;
68
+ }
69
+ const props = checker.getPropertiesOfType(sub);
70
+ for (const prop of props) {
71
+ const pName = prop.getName();
72
+ if (pName.startsWith('__')) {
73
+ const pType = checker.getTypeOfSymbolAtLocation(prop, prop.valueDeclaration || prop.declarations?.[0]);
74
+ let val = pType.value;
75
+ if (val === undefined && (pType.getFlags() & ts.TypeFlags.BooleanLiteral)) {
76
+ val = pType.intrinsicName === 'true';
77
+ }
78
+ if (val === undefined && (pType.getFlags() & ts.TypeFlags.Null)) {
79
+ val = null;
80
+ }
81
+ if (pName === '__default') {
82
+ constraints.push({ type: 'default', value: val });
83
+ }
84
+ else if (pName === '__message') {
85
+ constraints.push({ type: 'message', value: val });
86
+ }
87
+ else if (pName === '__transform_lowercase') {
88
+ constraints.push({ type: 'transform', value: 'lowercase' });
89
+ }
90
+ else if (pName === '__transform_uppercase') {
91
+ constraints.push({ type: 'transform', value: 'uppercase' });
92
+ }
93
+ else if (pName === '__transform_trim') {
94
+ constraints.push({ type: 'transform', value: 'trim' });
95
+ }
96
+ else if (pName === '__transform_capitalize') {
97
+ constraints.push({ type: 'transform', value: 'capitalize' });
98
+ }
99
+ else if (pName === '__transform_tonumber') {
100
+ constraints.push({ type: 'transform', value: 'tonumber' });
101
+ }
102
+ else if (pName === '__transform_toboolean') {
103
+ constraints.push({ type: 'transform', value: 'toboolean' });
104
+ }
105
+ else if (pName === '__transform_todate') {
106
+ constraints.push({ type: 'transform', value: 'todate' });
107
+ }
108
+ else if (pName === '__transform_custom') {
109
+ let fnName;
110
+ let filePath;
111
+ const symbol = pType.getSymbol() || pType.aliasSymbol;
112
+ if (symbol) {
113
+ fnName = symbol.getName();
114
+ let dec = symbol.valueDeclaration || symbol.declarations?.[0];
115
+ if (fnName === '__function' && dec) {
116
+ let current = dec;
117
+ while (current) {
118
+ if (ts.isVariableDeclaration(current) && ts.isIdentifier(current.name)) {
119
+ fnName = current.name.text;
120
+ dec = current;
121
+ break;
122
+ }
123
+ current = current.parent;
124
+ }
125
+ }
126
+ if (dec) {
127
+ const sourceFile = dec.getSourceFile();
128
+ if (sourceFile) {
129
+ filePath = sourceFile.fileName;
130
+ }
131
+ }
132
+ }
133
+ else {
134
+ const str = checker.typeToString(pType);
135
+ const match = str.match(/typeof\s+([a-zA-Z0-9_]+)/);
136
+ if (match) {
137
+ fnName = match[1];
138
+ }
139
+ }
140
+ if (fnName === '__function' || !fnName) {
141
+ throw new Error('[Webergency] Custom validator must reference a named function via typeof (e.g. tag.Custom<typeof myFunc>).');
142
+ }
143
+ if (filePath) {
144
+ requiredUtils.add(`custom:${fnName}:${filePath}`);
145
+ }
146
+ constraints.push({ type: 'transform_custom', value: fnName });
147
+ }
148
+ else if (pName === '__custom') {
149
+ let fnName;
150
+ let filePath;
151
+ const symbol = pType.getSymbol() || pType.aliasSymbol;
152
+ if (symbol) {
153
+ fnName = symbol.getName();
154
+ let dec = symbol.valueDeclaration || symbol.declarations?.[0];
155
+ if (fnName === '__function' && dec) {
156
+ let current = dec;
157
+ while (current) {
158
+ if (ts.isVariableDeclaration(current) && ts.isIdentifier(current.name)) {
159
+ fnName = current.name.text;
160
+ dec = current;
161
+ break;
162
+ }
163
+ current = current.parent;
164
+ }
165
+ }
166
+ if (dec) {
167
+ const sourceFile = dec.getSourceFile();
168
+ if (sourceFile) {
169
+ filePath = sourceFile.fileName;
170
+ }
171
+ }
172
+ }
173
+ else {
174
+ const str = checker.typeToString(pType);
175
+ const match = str.match(/typeof\s+([a-zA-Z0-9_]+)/);
176
+ if (match) {
177
+ fnName = match[1];
178
+ }
179
+ }
180
+ if (fnName === '__function' || !fnName) {
181
+ throw new Error('[Webergency] Custom validator must reference a named function via typeof (e.g. tag.Custom<typeof myFunc>).');
182
+ }
183
+ if (filePath) {
184
+ requiredUtils.add(`custom:${fnName}:${filePath}`);
185
+ }
186
+ const msgProp = props.find(p => p.getName() === `${pName}_message`);
187
+ let constraintMsg;
188
+ if (msgProp) {
189
+ const msgType = checker.getTypeOfSymbolAtLocation(msgProp, msgProp.valueDeclaration || msgProp.declarations?.[0]);
190
+ constraintMsg = getStringLiteralValue(msgType);
191
+ }
192
+ constraints.push({ type: 'custom', value: fnName, message: constraintMsg });
193
+ }
194
+ else if (val !== undefined) {
195
+ const msgProp = props.find(p => p.getName() === `${pName}_message`);
196
+ let constraintMsg;
197
+ if (msgProp) {
198
+ const msgType = checker.getTypeOfSymbolAtLocation(msgProp, msgProp.valueDeclaration || msgProp.declarations?.[0]);
199
+ constraintMsg = getStringLiteralValue(msgType);
200
+ }
201
+ if (pName === '__minLength') {
202
+ constraints.push({ type: 'minLength', value: val, message: constraintMsg });
203
+ }
204
+ else if (pName === '__maxLength') {
205
+ constraints.push({ type: 'maxLength', value: val, message: constraintMsg });
206
+ }
207
+ else if (pName === '__minimum') {
208
+ constraints.push({ type: 'minimum', value: val, message: constraintMsg });
209
+ }
210
+ else if (pName === '__maximum') {
211
+ constraints.push({ type: 'maximum', value: val, message: constraintMsg });
212
+ }
213
+ else if (pName === '__exclusiveMinimum') {
214
+ constraints.push({ type: 'exclusiveMinimum', value: val, message: constraintMsg });
215
+ }
216
+ else if (pName === '__exclusiveMaximum') {
217
+ constraints.push({ type: 'exclusiveMaximum', value: val, message: constraintMsg });
218
+ }
219
+ else if (pName === '__multipleOf') {
220
+ constraints.push({ type: 'multipleOf', value: val, message: constraintMsg });
221
+ }
222
+ else if (pName === '__pattern') {
223
+ constraints.push({ type: 'pattern', value: val, message: constraintMsg });
224
+ }
225
+ else if (pName === '__format') {
226
+ constraints.push({ type: 'format', value: val, message: constraintMsg });
227
+ }
228
+ else if (pName === '__minItems') {
229
+ constraints.push({ type: 'minItems', value: val, message: constraintMsg });
230
+ }
231
+ else if (pName === '__maxItems') {
232
+ constraints.push({ type: 'maxItems', value: val, message: constraintMsg });
233
+ }
234
+ else if (pName === '__uniqueItems') {
235
+ constraints.push({ type: 'uniqueItems', value: true, message: constraintMsg });
236
+ }
237
+ }
238
+ else if (pName === '__requires') {
239
+ let reqVal;
240
+ if (pType.isStringLiteral()) {
241
+ reqVal = pType.value;
242
+ }
243
+ else {
244
+ const typeArgs = pType.typeArguments || [];
245
+ const items = [];
246
+ for (const arg of typeArgs) {
247
+ if (arg.isStringLiteral()) {
248
+ items.push(arg.value);
249
+ }
250
+ }
251
+ reqVal = items;
252
+ }
253
+ const msgProp = props.find(p => p.getName() === `${pName}_message`);
254
+ let constraintMsg;
255
+ if (msgProp) {
256
+ const msgType = checker.getTypeOfSymbolAtLocation(msgProp, msgProp.valueDeclaration || msgProp.declarations?.[0]);
257
+ constraintMsg = getStringLiteralValue(msgType);
258
+ }
259
+ constraints.push({ type: 'requires', value: reqVal, message: constraintMsg });
260
+ }
261
+ }
262
+ }
263
+ }
264
+ if (constraints.length > 0) {
265
+ if (baseName) {
266
+ if (baseName === 'array' && baseType) {
267
+ const baseValidator = buildValidator(baseType, checker, validatorsMap, requiredUtils);
268
+ result = createConstrainedPrimitiveCheck(baseName, constraints, requiredUtils, baseValidator);
269
+ }
270
+ else if (baseType && (baseType.getFlags() & ts.TypeFlags.TemplateLiteral)) {
271
+ const baseValidator = buildValidator(baseType, checker, validatorsMap, requiredUtils);
272
+ result = createConstrainedPrimitiveCheck(baseName, constraints, requiredUtils, baseValidator);
273
+ }
274
+ else {
275
+ result = createConstrainedPrimitiveCheck(baseName, constraints, requiredUtils);
276
+ }
277
+ }
278
+ else {
279
+ const nonConstraintTypes = types.filter(t => {
280
+ const props = checker.getPropertiesOfType(t);
281
+ return !props.some(p => p.getName().startsWith('__'));
282
+ });
283
+ let baseValidator;
284
+ if (nonConstraintTypes.length === 1) {
285
+ baseValidator = buildValidator(nonConstraintTypes[0], checker, validatorsMap, requiredUtils);
286
+ }
287
+ else if (nonConstraintTypes.length > 1) {
288
+ const checks = nonConstraintTypes.map(t => buildValidator(t, checker, validatorsMap, requiredUtils));
289
+ baseValidator = createIntersectionCheck(checks, requiredUtils);
290
+ }
291
+ if (baseValidator) {
292
+ result = createConstrainedPrimitiveCheck('any', constraints, requiredUtils, baseValidator);
293
+ }
294
+ else {
295
+ const checks = type.types.map(t => buildValidator(t, checker, validatorsMap, requiredUtils));
296
+ result = createIntersectionCheck(checks, requiredUtils);
297
+ }
298
+ }
299
+ }
300
+ else {
301
+ const checks = type.types.map(t => buildValidator(t, checker, validatorsMap, requiredUtils));
302
+ result = createIntersectionCheck(checks, requiredUtils);
303
+ }
304
+ }
305
+ else if (type.getSymbol()?.name === 'Date') {
306
+ result = createDateCheck(requiredUtils);
307
+ }
308
+ else if (type.getSymbol()?.name === 'RegExp') {
309
+ result = createRegExpCheck(requiredUtils);
310
+ }
311
+ else if (type.getSymbol()?.name === 'Set') {
312
+ const elementType = type.typeArguments?.[0] || checker.getAnyType();
313
+ result = createSetCheck(buildValidator(elementType, checker, validatorsMap, requiredUtils), requiredUtils);
314
+ }
315
+ else if (type.getSymbol()?.name === 'Map') {
316
+ const keyType = type.typeArguments?.[0] || checker.getAnyType();
317
+ const valueType = type.typeArguments?.[1] || checker.getAnyType();
318
+ result = createMapCheck(buildValidator(keyType, checker, validatorsMap, requiredUtils), buildValidator(valueType, checker, validatorsMap, requiredUtils), requiredUtils);
319
+ }
320
+ else if (flags & ts.TypeFlags.Null) {
321
+ result = createNullCheck(requiredUtils);
322
+ }
323
+ else if (flags & ts.TypeFlags.Undefined || flags & ts.TypeFlags.Void) {
324
+ result = createUndefinedCheck(requiredUtils);
325
+ }
326
+ else if (flags & ts.TypeFlags.String) {
327
+ result = createPrimitiveCheck('string', requiredUtils);
328
+ }
329
+ else if (flags & ts.TypeFlags.Number) {
330
+ result = createPrimitiveCheck('number', requiredUtils);
331
+ }
332
+ else if (flags & ts.TypeFlags.BigInt) {
333
+ result = createPrimitiveCheck('bigint', requiredUtils);
334
+ }
335
+ else if (flags & ts.TypeFlags.Boolean) {
336
+ result = createPrimitiveCheck('boolean', requiredUtils);
337
+ }
338
+ else if (flags & ts.TypeFlags.TemplateLiteral) {
339
+ const templateType = type;
340
+ let regexStr = '^';
341
+ for (let i = 0; i < templateType.texts.length; i++) {
342
+ regexStr += templateType.texts[i].replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
343
+ if (i < templateType.types.length) {
344
+ const subType = templateType.types[i];
345
+ const subFlags = subType.getFlags();
346
+ if (subFlags & ts.TypeFlags.String) {
347
+ regexStr += '.*';
348
+ }
349
+ else if (subFlags & ts.TypeFlags.Number) {
350
+ regexStr += '[0-9]+(\\.[0-9]+)?';
351
+ }
352
+ else if (subFlags & ts.TypeFlags.BigInt) {
353
+ regexStr += '[0-9]+';
354
+ }
355
+ else if (subFlags & ts.TypeFlags.Boolean) {
356
+ regexStr += '(true|false)';
357
+ }
358
+ else {
359
+ regexStr += '.*';
360
+ }
361
+ }
362
+ }
363
+ regexStr += '$';
364
+ result = createTemplateLiteralCheck(regexStr, checker.typeToString(type), requiredUtils);
365
+ }
366
+ else if (type.isStringLiteral()) {
367
+ result = createLiteralCheck(type.value, requiredUtils);
368
+ }
369
+ else if (type.isNumberLiteral()) {
370
+ result = createLiteralCheck(type.value, requiredUtils);
371
+ }
372
+ else if (flags & ts.TypeFlags.BooleanLiteral) {
373
+ result = createLiteralCheck(type.intrinsicName === 'true', requiredUtils);
374
+ }
375
+ else if (flags & ts.TypeFlags.BigIntLiteral) {
376
+ result = createLiteralCheck(type.value, requiredUtils);
377
+ }
378
+ else if (checker.isTupleType(type)) {
379
+ const typeArgs = type.typeArguments || [];
380
+ result = createTupleCheck(typeArgs.map(t => buildValidator(t, checker, validatorsMap, requiredUtils)), requiredUtils);
381
+ }
382
+ else if (checker.isArrayType(type)) {
383
+ const elementType = type.typeArguments?.[0] || checker.getAnyType();
384
+ result = createArrayCheck(buildValidator(elementType, checker, validatorsMap, requiredUtils), requiredUtils);
385
+ }
386
+ else {
387
+ const stringIndexInfo = checker.getIndexInfoOfType(type, ts.IndexKind.String);
388
+ if (stringIndexInfo) {
389
+ result = createRecordCheck(buildValidator(stringIndexInfo.type, checker, validatorsMap, requiredUtils), requiredUtils);
390
+ }
391
+ else if (flags & ts.TypeFlags.Object || type.isClassOrInterface() || type.isTypeParameter()) {
392
+ const props = checker.getPropertiesOfType(type).map(prop => {
393
+ const declaration = prop.valueDeclaration || prop.declarations?.[0];
394
+ const propType = declaration ? checker.getTypeOfSymbolAtLocation(prop, declaration) : checker.getAnyType();
395
+ return {
396
+ name: prop.getName(),
397
+ isOptional: (prop.getFlags() & ts.SymbolFlags.Optional) !== 0,
398
+ validator: buildValidator(propType, checker, validatorsMap, requiredUtils)
399
+ };
400
+ });
401
+ const typeName = checker.typeToString(type);
402
+ result = createObjectCheck(props, requiredUtils, typeName);
403
+ }
404
+ else {
405
+ result = createPrimitiveCheck('any', requiredUtils);
406
+ }
407
+ }
408
+ validatorsMap.delete(hash);
409
+ validatorsMap.set(hash, result);
410
+ return ts.factory.createIdentifier(`__val_${hash}`);
411
+ }
412
+ function buildStructuralSignature(type, checker, visited = new Set()) {
413
+ const flags = type.getFlags();
414
+ const typeId = type.id;
415
+ if (typeId && visited.has(typeId)) {
416
+ return `[Circular:${typeId}]`;
417
+ }
418
+ if (typeId) {
419
+ visited.add(typeId);
420
+ }
421
+ if ((flags & ts.TypeFlags.Union) && type.types) {
422
+ return `Union<${type.types.map(t => buildStructuralSignature(t, checker, visited)).sort().join(',')}>`;
423
+ }
424
+ if ((flags & ts.TypeFlags.Intersection) && type.types) {
425
+ return `Intersection<${type.types.map(t => buildStructuralSignature(t, checker, visited)).sort().join(',')}>`;
426
+ }
427
+ if (type.isStringLiteral()) {
428
+ return `S:"${type.value}"`;
429
+ }
430
+ if (type.isNumberLiteral()) {
431
+ return `N:${type.value}`;
432
+ }
433
+ if (flags & ts.TypeFlags.BigIntLiteral) {
434
+ return `B:${checker.typeToString(type)}`;
435
+ }
436
+ if (flags & ts.TypeFlags.BooleanLiteral) {
437
+ return `L:${type.intrinsicName}`;
438
+ }
439
+ if (flags & ts.TypeFlags.String) {
440
+ return 'string';
441
+ }
442
+ if (flags & ts.TypeFlags.Number) {
443
+ return 'number';
444
+ }
445
+ if (flags & ts.TypeFlags.Boolean || type.intrinsicName === 'boolean') {
446
+ return 'boolean';
447
+ }
448
+ if (flags & ts.TypeFlags.BigInt) {
449
+ return 'bigint';
450
+ }
451
+ if (flags & ts.TypeFlags.Null) {
452
+ return 'null';
453
+ }
454
+ if (flags & ts.TypeFlags.Undefined || flags & ts.TypeFlags.Void) {
455
+ return 'undefined';
456
+ }
457
+ if (flags & ts.TypeFlags.TemplateLiteral) {
458
+ const templateType = type;
459
+ return `TemplateLiteral<${templateType.texts.join(',')}|${templateType.types.map(t => buildStructuralSignature(t, checker, visited)).join(',')}>`;
460
+ }
461
+ if (checker.isArrayType(type)) {
462
+ const elementType = type.typeArguments?.[0] || checker.getAnyType();
463
+ return `Array<${buildStructuralSignature(elementType, checker, visited)}>`;
464
+ }
465
+ if (flags & ts.TypeFlags.Object || type.isClassOrInterface()) {
466
+ const props = checker.getPropertiesOfType(type);
467
+ if (props.length === 0) {
468
+ // Handle Record or empty object
469
+ const stringIndexInfo = checker.getIndexInfoOfType(type, ts.IndexKind.String);
470
+ if (stringIndexInfo) {
471
+ return `Record<${buildStructuralSignature(stringIndexInfo.type, checker, visited)}>`;
472
+ }
473
+ }
474
+ const propSigs = props.map(prop => {
475
+ const declaration = prop.valueDeclaration || prop.declarations?.[0];
476
+ const propType = declaration ? checker.getTypeOfSymbolAtLocation(prop, declaration) : checker.getAnyType();
477
+ const isOptional = (prop.getFlags() & ts.SymbolFlags.Optional) !== 0;
478
+ return `${prop.getName()}${isOptional ? '?' : ''}:${buildStructuralSignature(propType, checker, visited)}`;
479
+ }).sort();
480
+ return `Object{${propSigs.join(';')}}`;
481
+ }
482
+ return 'any';
483
+ }
484
+ export function generateHash(type, checker) {
485
+ const structuralSig = buildStructuralSignature(type, checker);
486
+ return createHash('sha256').update(structuralSig).digest('hex').substring(0, 16);
487
+ }
488
+ export function objectToAst(val) {
489
+ if (val === null) {
490
+ return ts.factory.createNull();
491
+ }
492
+ if (val === undefined) {
493
+ return ts.factory.createIdentifier('undefined');
494
+ }
495
+ if (typeof val === 'string') {
496
+ return ts.factory.createStringLiteral(val);
497
+ }
498
+ if (typeof val === 'number') {
499
+ return ts.factory.createNumericLiteral(val.toString());
500
+ }
501
+ if (typeof val === 'boolean') {
502
+ return val ? ts.factory.createTrue() : ts.factory.createFalse();
503
+ }
504
+ if (typeof val === 'bigint') {
505
+ return ts.factory.createBigIntLiteral(val.toString() + 'n');
506
+ }
507
+ if (Array.isArray(val)) {
508
+ return ts.factory.createArrayLiteralExpression(val.map(objectToAst));
509
+ }
510
+ if (typeof val === 'object') {
511
+ const properties = Object.entries(val).map(([k, v]) => ts.factory.createPropertyAssignment(ts.factory.createStringLiteral(k), objectToAst(v)));
512
+ return ts.factory.createObjectLiteralExpression(properties, true);
513
+ }
514
+ return ts.factory.createIdentifier('undefined');
515
+ }
516
+ export function getTypeComplexity(type, checker, visited) {
517
+ const typeId = type.id;
518
+ if (typeId) {
519
+ if (visited.has(typeId)) {
520
+ return 1;
521
+ }
522
+ visited.add(typeId);
523
+ }
524
+ const flags = type.getFlags();
525
+ let complexity = 1;
526
+ const isUnion = (((flags & ts.TypeFlags.Union) !== 0 || type.isUnion()) && type.types) ? true : false;
527
+ const isIntersection = (((flags & ts.TypeFlags.Intersection) !== 0 || type.isIntersection()) && type.types) ? true : false;
528
+ if (isUnion) {
529
+ for (const t of type.types) {
530
+ complexity += getTypeComplexity(t, checker, visited);
531
+ }
532
+ }
533
+ else if (isIntersection) {
534
+ for (const t of type.types) {
535
+ complexity += getTypeComplexity(t, checker, visited);
536
+ }
537
+ }
538
+ else if (checker.isArrayType(type)) {
539
+ const elementType = type.typeArguments?.[0] || checker.getAnyType();
540
+ complexity += getTypeComplexity(elementType, checker, visited);
541
+ }
542
+ else if (checker.isTupleType(type)) {
543
+ const elementTypes = type.typeArguments || [];
544
+ for (const t of elementTypes) {
545
+ complexity += getTypeComplexity(t, checker, visited);
546
+ }
547
+ }
548
+ else if (flags & ts.TypeFlags.Object || type.isClassOrInterface()) {
549
+ const name = type.getSymbol()?.name;
550
+ if (name !== 'Date' && name !== 'Set' && name !== 'Map' && name !== 'RegExp') {
551
+ const props = checker.getPropertiesOfType(type);
552
+ for (const prop of props) {
553
+ const propType = checker.getTypeOfSymbolAtLocation(prop, prop.valueDeclaration || prop.declarations?.[0]);
554
+ complexity += getTypeComplexity(propType, checker, visited);
555
+ }
556
+ }
557
+ }
558
+ if (typeId) {
559
+ visited.delete(typeId);
560
+ }
561
+ return complexity;
562
+ }
563
+ export function preScanType(type, checker, counts, circularHashes, visited) {
564
+ const flags = type.getFlags();
565
+ const typeId = type.id;
566
+ if (!typeId) {
567
+ return;
568
+ }
569
+ if (visited.has(typeId)) {
570
+ const hash = generateHash(type, checker);
571
+ circularHashes.add(hash);
572
+ return;
573
+ }
574
+ visited.add(typeId);
575
+ const isUnion = (((flags & ts.TypeFlags.Union) !== 0 || type.isUnion()) && type.types) ? true : false;
576
+ const isIntersection = (((flags & ts.TypeFlags.Intersection) !== 0 || type.isIntersection()) && type.types) ? true : false;
577
+ if (isUnion) {
578
+ for (const t of type.types) {
579
+ preScanType(t, checker, counts, circularHashes, visited);
580
+ }
581
+ }
582
+ else if (isIntersection) {
583
+ for (const t of type.types) {
584
+ preScanType(t, checker, counts, circularHashes, visited);
585
+ }
586
+ }
587
+ else if (checker.isArrayType(type)) {
588
+ const elementType = type.typeArguments?.[0] || checker.getAnyType();
589
+ preScanType(elementType, checker, counts, circularHashes, visited);
590
+ }
591
+ else if (checker.isTupleType(type)) {
592
+ const elementTypes = type.typeArguments || [];
593
+ for (const t of elementTypes) {
594
+ preScanType(t, checker, counts, circularHashes, visited);
595
+ }
596
+ }
597
+ else if (flags & ts.TypeFlags.Object || type.isClassOrInterface()) {
598
+ const name = type.getSymbol()?.name;
599
+ if (name !== 'Date' && name !== 'Set' && name !== 'Map' && name !== 'RegExp') {
600
+ const hash = generateHash(type, checker);
601
+ counts.set(hash, (counts.get(hash) || 0) + 1);
602
+ const props = checker.getPropertiesOfType(type);
603
+ for (const prop of props) {
604
+ const propType = checker.getTypeOfSymbolAtLocation(prop, prop.valueDeclaration || prop.declarations?.[0]);
605
+ preScanType(propType, checker, counts, circularHashes, visited);
606
+ }
607
+ }
608
+ }
609
+ visited.delete(typeId);
610
+ }
611
+ export function buildJsonSchema(type, checker) {
612
+ const defs = {};
613
+ const visited = new Map();
614
+ const counts = new Map();
615
+ const circularHashes = new Set();
616
+ preScanType(type, checker, counts, circularHashes, new Set());
617
+ const rootSchema = buildJsonSchemaInternal(type, checker, defs, visited, counts, circularHashes);
618
+ if (Object.keys(defs).length > 0) {
619
+ const rootSymbol = type.getSymbol() || type.aliasSymbol;
620
+ const rootName = rootSymbol ? rootSymbol.getName() : 'Root';
621
+ const rootHash = generateHash(type, checker);
622
+ const rootDefName = `${rootName}_${rootHash}`;
623
+ if (defs[rootDefName]) {
624
+ return {
625
+ $ref: `#/$defs/${rootDefName}`,
626
+ $defs: defs
627
+ };
628
+ }
629
+ else {
630
+ return {
631
+ ...rootSchema,
632
+ $defs: defs
633
+ };
634
+ }
635
+ }
636
+ return rootSchema;
637
+ }
638
+ function buildJsonSchemaInternal(type, checker, defs, visited, counts, circularHashes) {
639
+ const flags = type.getFlags();
640
+ const typeId = type.id;
641
+ if (typeId && visited.has(typeId)) {
642
+ return { $ref: `#/$defs/${visited.get(typeId)}` };
643
+ }
644
+ const isUnion = (((flags & ts.TypeFlags.Union) !== 0 || type.isUnion()) && type.types) ? true : false;
645
+ const isIntersection = (((flags & ts.TypeFlags.Intersection) !== 0 || type.isIntersection()) && type.types) ? true : false;
646
+ if (isUnion) {
647
+ const types = type.types;
648
+ const isBoolUnion = types.length === 2 &&
649
+ types.every(t => (t.getFlags() & ts.TypeFlags.BooleanLiteral) !== 0);
650
+ if (isBoolUnion) {
651
+ return { type: 'boolean' };
652
+ }
653
+ return {
654
+ anyOf: types.map(t => buildJsonSchemaInternal(t, checker, defs, visited, counts, circularHashes))
655
+ };
656
+ }
657
+ if (isIntersection) {
658
+ const types = type.types;
659
+ let baseSchema = {};
660
+ const constraints = {};
661
+ for (const sub of types) {
662
+ const sFlags = sub.getFlags();
663
+ if (sFlags & ts.TypeFlags.String || sFlags & ts.TypeFlags.TemplateLiteral) {
664
+ baseSchema = { type: 'string' };
665
+ }
666
+ else if (sFlags & ts.TypeFlags.Number) {
667
+ baseSchema = { type: 'number' };
668
+ }
669
+ else if (sFlags & ts.TypeFlags.BigInt) {
670
+ baseSchema = { type: 'integer' };
671
+ }
672
+ else if (sFlags & ts.TypeFlags.Boolean || sub.intrinsicName === 'boolean') {
673
+ baseSchema = { type: 'boolean' };
674
+ }
675
+ else if (sub.getSymbol()?.name === 'Date') {
676
+ baseSchema = { type: 'string', format: 'date-time' };
677
+ }
678
+ else if (sub.getSymbol()?.name === 'RegExp') {
679
+ baseSchema = { type: 'string', format: 'regex' };
680
+ }
681
+ else if (sub.getSymbol()?.name === 'Set') {
682
+ const elementType = sub.typeArguments?.[0] || checker.getAnyType();
683
+ baseSchema = { type: 'array', items: buildJsonSchemaInternal(elementType, checker, defs, visited, counts, circularHashes), uniqueItems: true };
684
+ }
685
+ else if (sub.getSymbol()?.name === 'Map') {
686
+ const valueType = sub.typeArguments?.[1] || checker.getAnyType();
687
+ baseSchema = { type: 'object', additionalProperties: buildJsonSchemaInternal(valueType, checker, defs, visited, counts, circularHashes) };
688
+ }
689
+ else if (checker.isArrayType(sub)) {
690
+ const elementType = sub.typeArguments?.[0] || checker.getAnyType();
691
+ baseSchema = { type: 'array', items: buildJsonSchemaInternal(elementType, checker, defs, visited, counts, circularHashes) };
692
+ }
693
+ const props = checker.getPropertiesOfType(sub);
694
+ for (const prop of props) {
695
+ const pName = prop.getName();
696
+ if (pName.startsWith('__')) {
697
+ const pType = checker.getTypeOfSymbolAtLocation(prop, prop.valueDeclaration || prop.declarations?.[0]);
698
+ let val = pType.value;
699
+ if (val === undefined && (pType.getFlags() & ts.TypeFlags.BooleanLiteral)) {
700
+ val = pType.intrinsicName === 'true';
701
+ }
702
+ if (val === undefined && (pType.getFlags() & ts.TypeFlags.Null)) {
703
+ val = null;
704
+ }
705
+ if (pName === '__default') {
706
+ constraints.default = val;
707
+ }
708
+ else if (pName === '__minLength') {
709
+ constraints.minLength = val;
710
+ }
711
+ else if (pName === '__maxLength') {
712
+ constraints.maxLength = val;
713
+ }
714
+ else if (pName === '__minimum') {
715
+ constraints.minimum = val;
716
+ }
717
+ else if (pName === '__maximum') {
718
+ constraints.maximum = val;
719
+ }
720
+ else if (pName === '__exclusiveMinimum') {
721
+ constraints.exclusiveMinimum = val;
722
+ }
723
+ else if (pName === '__exclusiveMaximum') {
724
+ constraints.exclusiveMaximum = val;
725
+ }
726
+ else if (pName === '__multipleOf') {
727
+ constraints.multipleOf = val;
728
+ }
729
+ else if (pName === '__pattern') {
730
+ constraints.pattern = val;
731
+ }
732
+ else if (pName === '__format') {
733
+ constraints.format = val;
734
+ }
735
+ else if (pName === '__minItems') {
736
+ constraints.minItems = val;
737
+ }
738
+ else if (pName === '__maxItems') {
739
+ constraints.maxItems = val;
740
+ }
741
+ else if (pName === '__uniqueItems') {
742
+ constraints.uniqueItems = true;
743
+ }
744
+ else if (pName === '__requires') {
745
+ let reqVal;
746
+ if (pType.isStringLiteral()) {
747
+ reqVal = pType.value;
748
+ }
749
+ else {
750
+ const typeArgs = pType.typeArguments || [];
751
+ const items = [];
752
+ for (const arg of typeArgs) {
753
+ if (arg.isStringLiteral()) {
754
+ items.push(arg.value);
755
+ }
756
+ }
757
+ reqVal = items;
758
+ }
759
+ constraints.requires = reqVal;
760
+ }
761
+ }
762
+ }
763
+ }
764
+ return { ...baseSchema, ...constraints };
765
+ }
766
+ if (type.getSymbol()?.name === 'Date') {
767
+ return { type: 'string', format: 'date-time' };
768
+ }
769
+ if (type.getSymbol()?.name === 'RegExp') {
770
+ return { type: 'string', format: 'regex' };
771
+ }
772
+ if (type.getSymbol()?.name === 'Set') {
773
+ const elementType = type.typeArguments?.[0] || checker.getAnyType();
774
+ return {
775
+ type: 'array',
776
+ items: buildJsonSchemaInternal(elementType, checker, defs, visited, counts, circularHashes),
777
+ uniqueItems: true
778
+ };
779
+ }
780
+ if (type.getSymbol()?.name === 'Map') {
781
+ const valueType = type.typeArguments?.[1] || checker.getAnyType();
782
+ return {
783
+ type: 'object',
784
+ additionalProperties: buildJsonSchemaInternal(valueType, checker, defs, visited, counts, circularHashes)
785
+ };
786
+ }
787
+ if (flags & ts.TypeFlags.Null) {
788
+ return { type: 'null' };
789
+ }
790
+ if (flags & ts.TypeFlags.Undefined || flags & ts.TypeFlags.Void) {
791
+ return { type: 'null', description: 'undefined' };
792
+ }
793
+ if (flags & ts.TypeFlags.String) {
794
+ return { type: 'string' };
795
+ }
796
+ if (flags & ts.TypeFlags.Number) {
797
+ return { type: 'number' };
798
+ }
799
+ if (flags & ts.TypeFlags.BigInt) {
800
+ return { type: 'integer' };
801
+ }
802
+ if (flags & ts.TypeFlags.Boolean || type.intrinsicName === 'boolean') {
803
+ return { type: 'boolean' };
804
+ }
805
+ if (type.isStringLiteral()) {
806
+ return { type: 'string', const: type.value };
807
+ }
808
+ if (type.isNumberLiteral()) {
809
+ return { type: 'number', const: type.value };
810
+ }
811
+ if (flags & ts.TypeFlags.BigIntLiteral) {
812
+ return { type: 'integer', const: type.value };
813
+ }
814
+ if (flags & ts.TypeFlags.BooleanLiteral) {
815
+ return { type: 'boolean', const: type.intrinsicName === 'true' };
816
+ }
817
+ if (checker.isArrayType(type)) {
818
+ const elementType = type.typeArguments?.[0] || checker.getAnyType();
819
+ return {
820
+ type: 'array',
821
+ items: buildJsonSchemaInternal(elementType, checker, defs, visited, counts, circularHashes)
822
+ };
823
+ }
824
+ if (checker.isTupleType(type)) {
825
+ const elementTypes = type.typeArguments || [];
826
+ return {
827
+ type: 'array',
828
+ items: elementTypes.map(t => buildJsonSchemaInternal(t, checker, defs, visited, counts, circularHashes)),
829
+ minItems: elementTypes.length,
830
+ maxItems: elementTypes.length
831
+ };
832
+ }
833
+ // Object types
834
+ if (flags & ts.TypeFlags.Object || type.isClassOrInterface()) {
835
+ const symbol = type.getSymbol() || type.aliasSymbol;
836
+ const name = symbol ? symbol.getName() : 'Object';
837
+ const typeHash = generateHash(type, checker);
838
+ const defName = `${name}_${typeHash}`;
839
+ if (defs[defName]) {
840
+ return { $ref: `#/$defs/${defName}` };
841
+ }
842
+ if (typeId && visited.has(typeId)) {
843
+ return { $ref: `#/$defs/${defName}` };
844
+ }
845
+ if (typeId) {
846
+ visited.set(typeId, defName);
847
+ }
848
+ const properties = {};
849
+ const required = [];
850
+ const props = checker.getPropertiesOfType(type);
851
+ for (const prop of props) {
852
+ const pName = prop.getName();
853
+ const isOptional = (prop.flags & ts.SymbolFlags.Optional) !== 0;
854
+ const propType = checker.getTypeOfSymbolAtLocation(prop, prop.valueDeclaration || prop.declarations?.[0]);
855
+ properties[pName] = buildJsonSchemaInternal(propType, checker, defs, visited, counts, circularHashes);
856
+ if (!isOptional) {
857
+ required.push(pName);
858
+ }
859
+ }
860
+ const schemaObj = {
861
+ type: 'object',
862
+ properties,
863
+ additionalProperties: false
864
+ };
865
+ if (required.length > 0) {
866
+ schemaObj.required = required;
867
+ }
868
+ if (typeId) {
869
+ visited.delete(typeId);
870
+ }
871
+ const isCircular = circularHashes.has(typeHash);
872
+ const refCount = counts.get(typeHash) || 0;
873
+ const complexity = getTypeComplexity(type, checker, new Set());
874
+ const score = refCount * complexity;
875
+ if (isCircular || score >= 128) {
876
+ defs[defName] = schemaObj;
877
+ return { $ref: `#/$defs/${defName}` };
878
+ }
879
+ return schemaObj;
880
+ }
881
+ return {};
882
+ }