@flisk/analyze-tracking 0.9.0 → 0.9.1
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/README.md +36 -0
- package/package.json +1 -1
- package/schema.json +10 -0
- package/src/analyze/javascript/detectors/analytics-source.js +38 -4
- package/src/analyze/javascript/extractors/event-extractor.js +29 -12
- package/src/analyze/javascript/parser.js +26 -8
- package/src/analyze/typescript/detectors/analytics-source.js +37 -3
- package/src/analyze/typescript/extractors/event-extractor.js +59 -31
- package/src/analyze/typescript/extractors/property-extractor.js +129 -91
- package/src/analyze/typescript/parser.js +27 -8
- package/src/analyze/typescript/utils/type-resolver.js +600 -21
- package/src/analyze/utils/customFunctionParser.js +29 -0
|
@@ -18,24 +18,24 @@ function resolveIdentifierToInitializer(checker, identifier, sourceFile) {
|
|
|
18
18
|
if (!symbol || !symbol.valueDeclaration) {
|
|
19
19
|
return null;
|
|
20
20
|
}
|
|
21
|
-
|
|
21
|
+
|
|
22
22
|
const declaration = symbol.valueDeclaration;
|
|
23
|
-
|
|
23
|
+
|
|
24
24
|
// Handle variable declarations
|
|
25
25
|
if (ts.isVariableDeclaration(declaration) && declaration.initializer) {
|
|
26
26
|
return declaration.initializer;
|
|
27
27
|
}
|
|
28
|
-
|
|
28
|
+
|
|
29
29
|
// Handle property assignments
|
|
30
30
|
if (ts.isPropertyAssignment(declaration) && declaration.initializer) {
|
|
31
31
|
return declaration.initializer;
|
|
32
32
|
}
|
|
33
|
-
|
|
33
|
+
|
|
34
34
|
// Handle parameter with default value
|
|
35
35
|
if (ts.isParameter(declaration) && declaration.initializer) {
|
|
36
36
|
return declaration.initializer;
|
|
37
37
|
}
|
|
38
|
-
|
|
38
|
+
|
|
39
39
|
return null;
|
|
40
40
|
} catch (error) {
|
|
41
41
|
return null;
|
|
@@ -69,12 +69,12 @@ function resolveTypeToProperties(checker, typeString, visitedTypes = new Set())
|
|
|
69
69
|
if (visitedTypes.has(typeString)) {
|
|
70
70
|
return { type: 'object' };
|
|
71
71
|
}
|
|
72
|
-
|
|
72
|
+
|
|
73
73
|
// Handle primitive types
|
|
74
74
|
if (['string', 'number', 'boolean', 'any', 'unknown', 'null', 'undefined', 'void', 'never'].includes(typeString)) {
|
|
75
75
|
return { type: typeString };
|
|
76
76
|
}
|
|
77
|
-
|
|
77
|
+
|
|
78
78
|
// Handle array types: T[] or Array<T>
|
|
79
79
|
const arrayMatch = typeString.match(/^(.+)\[\]$/) || typeString.match(/^Array<(.+)>$/);
|
|
80
80
|
if (arrayMatch) {
|
|
@@ -86,7 +86,7 @@ function resolveTypeToProperties(checker, typeString, visitedTypes = new Set())
|
|
|
86
86
|
items: elementProps
|
|
87
87
|
};
|
|
88
88
|
}
|
|
89
|
-
|
|
89
|
+
|
|
90
90
|
// Handle readonly array types: readonly T[] or ReadonlyArray<T>
|
|
91
91
|
const readonlyArrayMatch = typeString.match(/^readonly (.+)\[\]$/) || typeString.match(/^ReadonlyArray<(.+)>$/);
|
|
92
92
|
if (readonlyArrayMatch) {
|
|
@@ -98,18 +98,37 @@ function resolveTypeToProperties(checker, typeString, visitedTypes = new Set())
|
|
|
98
98
|
items: elementProps
|
|
99
99
|
};
|
|
100
100
|
}
|
|
101
|
-
|
|
102
|
-
// Handle union types
|
|
101
|
+
|
|
102
|
+
// Handle union types
|
|
103
103
|
if (typeString.includes('|')) {
|
|
104
|
+
// Try to extract the non-undefined/non-null type from the union
|
|
105
|
+
const resolvedUnion = resolveUnionType(checker, typeString, visitedTypes);
|
|
106
|
+
if (resolvedUnion) {
|
|
107
|
+
return resolvedUnion;
|
|
108
|
+
}
|
|
109
|
+
// Fallback: preserve as-is
|
|
104
110
|
return { type: typeString };
|
|
105
111
|
}
|
|
106
|
-
|
|
112
|
+
|
|
107
113
|
// Handle intersection types
|
|
108
114
|
if (typeString.includes('&')) {
|
|
109
115
|
// For simplicity, mark intersection types as 'object'
|
|
110
116
|
return { type: 'object' };
|
|
111
117
|
}
|
|
112
|
-
|
|
118
|
+
|
|
119
|
+
// Check if it's an enum type - don't try to expand enum members
|
|
120
|
+
if (checker && isEnumType(checker, typeString)) {
|
|
121
|
+
const enumValues = getEnumValues(checker, typeString);
|
|
122
|
+
if (enumValues && enumValues.length > 0) {
|
|
123
|
+
return {
|
|
124
|
+
type: 'enum',
|
|
125
|
+
values: enumValues
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
// Fallback for string enums
|
|
129
|
+
return { type: 'string' };
|
|
130
|
+
}
|
|
131
|
+
|
|
113
132
|
// Check if it looks like a custom type/interface
|
|
114
133
|
if (isCustomType(typeString)) {
|
|
115
134
|
return {
|
|
@@ -117,11 +136,245 @@ function resolveTypeToProperties(checker, typeString, visitedTypes = new Set())
|
|
|
117
136
|
__unresolved: typeString
|
|
118
137
|
};
|
|
119
138
|
}
|
|
120
|
-
|
|
139
|
+
|
|
121
140
|
// Default case - preserve the type string as-is
|
|
122
141
|
return { type: typeString };
|
|
123
142
|
}
|
|
124
143
|
|
|
144
|
+
/**
|
|
145
|
+
* Resolves a union type by extracting the meaningful type (ignoring undefined/null)
|
|
146
|
+
* @param {Object} checker - TypeScript type checker
|
|
147
|
+
* @param {string} typeString - Union type string
|
|
148
|
+
* @param {Set} visitedTypes - Set of visited types
|
|
149
|
+
* @returns {Object|null} Resolved type or null
|
|
150
|
+
*/
|
|
151
|
+
function resolveUnionType(checker, typeString, visitedTypes) {
|
|
152
|
+
// Split by | and trim each part
|
|
153
|
+
const parts = splitUnionType(typeString);
|
|
154
|
+
|
|
155
|
+
// Filter out undefined and null
|
|
156
|
+
const meaningfulParts = parts.filter(p =>
|
|
157
|
+
p !== 'undefined' && p !== 'null' && p.trim() !== ''
|
|
158
|
+
);
|
|
159
|
+
|
|
160
|
+
if (meaningfulParts.length === 0) {
|
|
161
|
+
return { type: 'null' };
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (meaningfulParts.length === 1) {
|
|
165
|
+
const part = meaningfulParts[0].trim();
|
|
166
|
+
|
|
167
|
+
// Check if it's an object literal type like { id: string; name: string }
|
|
168
|
+
if (part.startsWith('{') && part.endsWith('}')) {
|
|
169
|
+
const properties = parseObjectLiteralType(part);
|
|
170
|
+
if (Object.keys(properties).length > 0) {
|
|
171
|
+
return {
|
|
172
|
+
type: 'object',
|
|
173
|
+
properties
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Recursively resolve the meaningful part
|
|
179
|
+
return resolveTypeToProperties(checker, part, visitedTypes);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Multiple meaningful parts - try to find the most specific one
|
|
183
|
+
// Prefer object types over primitives
|
|
184
|
+
for (const part of meaningfulParts) {
|
|
185
|
+
const trimmed = part.trim();
|
|
186
|
+
if (trimmed.startsWith('{') && trimmed.endsWith('}')) {
|
|
187
|
+
const properties = parseObjectLiteralType(trimmed);
|
|
188
|
+
if (Object.keys(properties).length > 0) {
|
|
189
|
+
return {
|
|
190
|
+
type: 'object',
|
|
191
|
+
properties
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// Return null to indicate we couldn't resolve it
|
|
198
|
+
return null;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Splits a union type string into its constituent parts, handling nested braces
|
|
203
|
+
* @param {string} typeString - Union type string
|
|
204
|
+
* @returns {string[]} Array of type parts
|
|
205
|
+
*/
|
|
206
|
+
function splitUnionType(typeString) {
|
|
207
|
+
const parts = [];
|
|
208
|
+
let current = '';
|
|
209
|
+
let depth = 0;
|
|
210
|
+
let parenDepth = 0;
|
|
211
|
+
let angleDepth = 0;
|
|
212
|
+
|
|
213
|
+
for (let i = 0; i < typeString.length; i++) {
|
|
214
|
+
const char = typeString[i];
|
|
215
|
+
|
|
216
|
+
if (char === '{') depth++;
|
|
217
|
+
else if (char === '}') depth--;
|
|
218
|
+
else if (char === '(') parenDepth++;
|
|
219
|
+
else if (char === ')') parenDepth--;
|
|
220
|
+
else if (char === '<') angleDepth++;
|
|
221
|
+
else if (char === '>') angleDepth--;
|
|
222
|
+
|
|
223
|
+
if (char === '|' && depth === 0 && parenDepth === 0 && angleDepth === 0) {
|
|
224
|
+
parts.push(current.trim());
|
|
225
|
+
current = '';
|
|
226
|
+
} else {
|
|
227
|
+
current += char;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
if (current.trim()) {
|
|
232
|
+
parts.push(current.trim());
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
return parts;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Parses an object literal type string like "{ id: string; name: string }"
|
|
240
|
+
* @param {string} typeString - Object literal type string
|
|
241
|
+
* @returns {Object} Parsed properties
|
|
242
|
+
*/
|
|
243
|
+
function parseObjectLiteralType(typeString) {
|
|
244
|
+
const properties = {};
|
|
245
|
+
|
|
246
|
+
// Remove outer braces and trim
|
|
247
|
+
let inner = typeString.slice(1, -1).trim();
|
|
248
|
+
if (!inner) return properties;
|
|
249
|
+
|
|
250
|
+
// Split by semicolons (property separators), handling nested braces
|
|
251
|
+
const propStrings = splitBySemicolon(inner);
|
|
252
|
+
|
|
253
|
+
for (const propString of propStrings) {
|
|
254
|
+
const trimmed = propString.trim();
|
|
255
|
+
if (!trimmed) continue;
|
|
256
|
+
|
|
257
|
+
// Parse "key: type" or "key?: type"
|
|
258
|
+
const colonIndex = findPropertyColonIndex(trimmed);
|
|
259
|
+
if (colonIndex === -1) continue;
|
|
260
|
+
|
|
261
|
+
let key = trimmed.slice(0, colonIndex).trim();
|
|
262
|
+
const typeStr = trimmed.slice(colonIndex + 1).trim();
|
|
263
|
+
|
|
264
|
+
// Handle optional properties (key?)
|
|
265
|
+
if (key.endsWith('?')) {
|
|
266
|
+
key = key.slice(0, -1);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
if (!key) continue;
|
|
270
|
+
|
|
271
|
+
// Resolve the property type
|
|
272
|
+
properties[key] = resolvePropertyType(typeStr);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
return properties;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Splits a string by semicolons, respecting nested structures
|
|
280
|
+
* @param {string} str - String to split
|
|
281
|
+
* @returns {string[]} Array of parts
|
|
282
|
+
*/
|
|
283
|
+
function splitBySemicolon(str) {
|
|
284
|
+
const parts = [];
|
|
285
|
+
let current = '';
|
|
286
|
+
let depth = 0;
|
|
287
|
+
let parenDepth = 0;
|
|
288
|
+
let angleDepth = 0;
|
|
289
|
+
|
|
290
|
+
for (let i = 0; i < str.length; i++) {
|
|
291
|
+
const char = str[i];
|
|
292
|
+
|
|
293
|
+
if (char === '{') depth++;
|
|
294
|
+
else if (char === '}') depth--;
|
|
295
|
+
else if (char === '(') parenDepth++;
|
|
296
|
+
else if (char === ')') parenDepth--;
|
|
297
|
+
else if (char === '<') angleDepth++;
|
|
298
|
+
else if (char === '>') angleDepth--;
|
|
299
|
+
|
|
300
|
+
if (char === ';' && depth === 0 && parenDepth === 0 && angleDepth === 0) {
|
|
301
|
+
parts.push(current.trim());
|
|
302
|
+
current = '';
|
|
303
|
+
} else {
|
|
304
|
+
current += char;
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
if (current.trim()) {
|
|
309
|
+
parts.push(current.trim());
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
return parts;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* Finds the index of the colon separating property name from type
|
|
317
|
+
* @param {string} str - Property string
|
|
318
|
+
* @returns {number} Index of the colon or -1
|
|
319
|
+
*/
|
|
320
|
+
function findPropertyColonIndex(str) {
|
|
321
|
+
// Find the first colon that's not inside nested structures
|
|
322
|
+
let depth = 0;
|
|
323
|
+
let parenDepth = 0;
|
|
324
|
+
let angleDepth = 0;
|
|
325
|
+
|
|
326
|
+
for (let i = 0; i < str.length; i++) {
|
|
327
|
+
const char = str[i];
|
|
328
|
+
|
|
329
|
+
if (char === '{') depth++;
|
|
330
|
+
else if (char === '}') depth--;
|
|
331
|
+
else if (char === '(') parenDepth++;
|
|
332
|
+
else if (char === ')') parenDepth--;
|
|
333
|
+
else if (char === '<') angleDepth++;
|
|
334
|
+
else if (char === '>') angleDepth--;
|
|
335
|
+
else if (char === ':' && depth === 0 && parenDepth === 0 && angleDepth === 0) {
|
|
336
|
+
return i;
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
return -1;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* Resolves a property type string to a schema
|
|
345
|
+
* @param {string} typeStr - Type string
|
|
346
|
+
* @returns {Object} Property schema
|
|
347
|
+
*/
|
|
348
|
+
function resolvePropertyType(typeStr) {
|
|
349
|
+
const trimmed = typeStr.trim();
|
|
350
|
+
|
|
351
|
+
// Handle primitive types
|
|
352
|
+
if (['string', 'number', 'boolean', 'any', 'unknown', 'null', 'undefined'].includes(trimmed)) {
|
|
353
|
+
return { type: trimmed };
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// Handle array types
|
|
357
|
+
if (trimmed.endsWith('[]')) {
|
|
358
|
+
const elementType = trimmed.slice(0, -2).trim();
|
|
359
|
+
return {
|
|
360
|
+
type: 'array',
|
|
361
|
+
items: resolvePropertyType(elementType)
|
|
362
|
+
};
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
// Handle nested object types
|
|
366
|
+
if (trimmed.startsWith('{') && trimmed.endsWith('}')) {
|
|
367
|
+
const nestedProps = parseObjectLiteralType(trimmed);
|
|
368
|
+
return {
|
|
369
|
+
type: 'object',
|
|
370
|
+
properties: nestedProps
|
|
371
|
+
};
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
// For other types, return as-is
|
|
375
|
+
return { type: trimmed };
|
|
376
|
+
}
|
|
377
|
+
|
|
125
378
|
/**
|
|
126
379
|
* Checks if a type string represents a custom type or interface
|
|
127
380
|
* @param {string} typeString - Type string to check
|
|
@@ -129,9 +382,9 @@ function resolveTypeToProperties(checker, typeString, visitedTypes = new Set())
|
|
|
129
382
|
*/
|
|
130
383
|
function isCustomType(typeString) {
|
|
131
384
|
// Custom types typically start with uppercase and don't contain certain characters
|
|
132
|
-
return typeString[0] === typeString[0].toUpperCase() &&
|
|
133
|
-
!typeString.includes('<') &&
|
|
134
|
-
!typeString.includes('|') &&
|
|
385
|
+
return typeString[0] === typeString[0].toUpperCase() &&
|
|
386
|
+
!typeString.includes('<') &&
|
|
387
|
+
!typeString.includes('|') &&
|
|
135
388
|
!typeString.includes('&') &&
|
|
136
389
|
!typeString.includes('(') &&
|
|
137
390
|
!typeString.includes('[');
|
|
@@ -145,7 +398,7 @@ function isCustomType(typeString) {
|
|
|
145
398
|
*/
|
|
146
399
|
function getBasicTypeOfArrayElement(checker, element) {
|
|
147
400
|
if (!element || typeof element.kind === 'undefined') return 'any';
|
|
148
|
-
|
|
401
|
+
|
|
149
402
|
// Check for literal values first
|
|
150
403
|
if (ts.isStringLiteral(element)) {
|
|
151
404
|
return 'string';
|
|
@@ -162,10 +415,10 @@ function getBasicTypeOfArrayElement(checker, element) {
|
|
|
162
415
|
} else if (element.kind === ts.SyntaxKind.UndefinedKeyword) {
|
|
163
416
|
return 'undefined';
|
|
164
417
|
}
|
|
165
|
-
|
|
418
|
+
|
|
166
419
|
// For identifiers and other expressions, try to get the type
|
|
167
420
|
const typeString = getTypeOfNode(checker, element);
|
|
168
|
-
|
|
421
|
+
|
|
169
422
|
// Extract basic type from TypeScript type string
|
|
170
423
|
if (typeString.startsWith('"') || typeString.startsWith("'")) {
|
|
171
424
|
return 'string'; // String literal type
|
|
@@ -180,7 +433,7 @@ function getBasicTypeOfArrayElement(checker, element) {
|
|
|
180
433
|
} else if (isCustomType(typeString)) {
|
|
181
434
|
return 'object';
|
|
182
435
|
}
|
|
183
|
-
|
|
436
|
+
|
|
184
437
|
return 'any';
|
|
185
438
|
}
|
|
186
439
|
|
|
@@ -198,11 +451,337 @@ function isReactHookCall(node, hookNames = ['useCallback', 'useState', 'useEffec
|
|
|
198
451
|
return false;
|
|
199
452
|
}
|
|
200
453
|
|
|
454
|
+
/**
|
|
455
|
+
* Checks if a type is an enum type by examining its symbol
|
|
456
|
+
* @param {Object} checker - TypeScript type checker
|
|
457
|
+
* @param {string} typeString - Type string to check
|
|
458
|
+
* @param {Object} [type] - Optional TypeScript type object
|
|
459
|
+
* @returns {boolean}
|
|
460
|
+
*/
|
|
461
|
+
function isEnumType(checker, typeString, type = null) {
|
|
462
|
+
if (!checker || !typeString) return false;
|
|
463
|
+
|
|
464
|
+
// Check if it's a simple enum type name (not a union)
|
|
465
|
+
if (typeString.includes('|') || typeString.includes('&')) {
|
|
466
|
+
return false;
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
// Skip primitive types
|
|
470
|
+
if (['string', 'number', 'boolean', 'any', 'unknown', 'null', 'undefined', 'void'].includes(typeString)) {
|
|
471
|
+
return false;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
// If we have a type object, check its symbol flags
|
|
475
|
+
if (type && type.symbol) {
|
|
476
|
+
// Check if the symbol has the Enum flag
|
|
477
|
+
if (type.symbol.flags & ts.SymbolFlags.Enum) {
|
|
478
|
+
return true;
|
|
479
|
+
}
|
|
480
|
+
// Check if the symbol has the EnumMember flag (for individual enum values)
|
|
481
|
+
if (type.symbol.flags & ts.SymbolFlags.EnumMember) {
|
|
482
|
+
return true;
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
return false;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
/**
|
|
490
|
+
* Gets the values of an enum type from its type object
|
|
491
|
+
* @param {Object} checker - TypeScript type checker
|
|
492
|
+
* @param {string} typeString - Enum type name
|
|
493
|
+
* @param {Object} [type] - Optional TypeScript type object
|
|
494
|
+
* @returns {string[]|null} Array of enum values or null
|
|
495
|
+
*/
|
|
496
|
+
function getEnumValues(checker, typeString, type = null) {
|
|
497
|
+
if (!checker || !typeString) return null;
|
|
498
|
+
|
|
499
|
+
try {
|
|
500
|
+
let enumSymbol = null;
|
|
501
|
+
|
|
502
|
+
if (type && type.symbol) {
|
|
503
|
+
// Check if this is an enum member (e.g., SubscriptionType.MONTHLY)
|
|
504
|
+
if (type.symbol.flags & ts.SymbolFlags.EnumMember) {
|
|
505
|
+
// Get the parent enum
|
|
506
|
+
enumSymbol = type.symbol.parent;
|
|
507
|
+
}
|
|
508
|
+
// Check if this is the enum type itself
|
|
509
|
+
else if (type.symbol.flags & ts.SymbolFlags.Enum) {
|
|
510
|
+
enumSymbol = type.symbol;
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
if (enumSymbol && enumSymbol.exports) {
|
|
515
|
+
const values = [];
|
|
516
|
+
enumSymbol.exports.forEach((member, name) => {
|
|
517
|
+
// Get the value of each enum member
|
|
518
|
+
if (member.declarations && member.declarations.length > 0) {
|
|
519
|
+
const decl = member.declarations[0];
|
|
520
|
+
if (ts.isEnumMember(decl) && decl.initializer) {
|
|
521
|
+
if (ts.isStringLiteral(decl.initializer)) {
|
|
522
|
+
values.push(decl.initializer.text);
|
|
523
|
+
} else if (ts.isNumericLiteral(decl.initializer)) {
|
|
524
|
+
values.push(Number(decl.initializer.text));
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
});
|
|
529
|
+
if (values.length > 0) {
|
|
530
|
+
return values;
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
} catch (e) {
|
|
534
|
+
// Ignore errors
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
return null;
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
/**
|
|
541
|
+
* Resolves a TypeScript type object to a property schema
|
|
542
|
+
* This is more accurate than resolving from type strings
|
|
543
|
+
* @param {Object} checker - TypeScript type checker
|
|
544
|
+
* @param {Object} type - TypeScript Type object
|
|
545
|
+
* @param {Set} [visitedTypes] - Set of visited types to prevent cycles
|
|
546
|
+
* @returns {Object} Property schema
|
|
547
|
+
*/
|
|
548
|
+
function resolveTypeObjectToSchema(checker, type, visitedTypes = new Set()) {
|
|
549
|
+
if (!type) return { type: 'any' };
|
|
550
|
+
|
|
551
|
+
const typeString = checker.typeToString(type);
|
|
552
|
+
|
|
553
|
+
// Prevent infinite recursion
|
|
554
|
+
if (visitedTypes.has(typeString)) {
|
|
555
|
+
return { type: 'object' };
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
// Handle union types
|
|
559
|
+
if (type.isUnion?.()) {
|
|
560
|
+
return resolveUnionTypeObject(checker, type, visitedTypes);
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
// Handle enum types (actual enum declarations)
|
|
564
|
+
if (isEnumType(checker, typeString, type)) {
|
|
565
|
+
const enumValues = getEnumValues(checker, typeString, type);
|
|
566
|
+
if (enumValues && enumValues.length > 0) {
|
|
567
|
+
return { type: 'enum', values: enumValues };
|
|
568
|
+
}
|
|
569
|
+
return { type: 'string' };
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
// Handle primitive types
|
|
573
|
+
const flags = type.flags;
|
|
574
|
+
if (flags & ts.TypeFlags.String || flags & ts.TypeFlags.StringLiteral) {
|
|
575
|
+
return { type: 'string' };
|
|
576
|
+
}
|
|
577
|
+
if (flags & ts.TypeFlags.Number || flags & ts.TypeFlags.NumberLiteral) {
|
|
578
|
+
return { type: 'number' };
|
|
579
|
+
}
|
|
580
|
+
if (flags & ts.TypeFlags.Boolean || flags & ts.TypeFlags.BooleanLiteral) {
|
|
581
|
+
return { type: 'boolean' };
|
|
582
|
+
}
|
|
583
|
+
if (flags & ts.TypeFlags.Undefined) {
|
|
584
|
+
return { type: 'undefined' };
|
|
585
|
+
}
|
|
586
|
+
if (flags & ts.TypeFlags.Null) {
|
|
587
|
+
return { type: 'null' };
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
// Handle array types
|
|
591
|
+
if (checker.isArrayType?.(type) || typeString.endsWith('[]') || typeString.startsWith('Array<')) {
|
|
592
|
+
let elementType = null;
|
|
593
|
+
if (type.typeArguments && type.typeArguments.length > 0) {
|
|
594
|
+
elementType = type.typeArguments[0];
|
|
595
|
+
}
|
|
596
|
+
if (elementType) {
|
|
597
|
+
visitedTypes.add(typeString);
|
|
598
|
+
return {
|
|
599
|
+
type: 'array',
|
|
600
|
+
items: resolveTypeObjectToSchema(checker, elementType, visitedTypes)
|
|
601
|
+
};
|
|
602
|
+
}
|
|
603
|
+
return { type: 'array', items: { type: 'any' } };
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
// Handle object types - try to extract properties
|
|
607
|
+
if (flags & ts.TypeFlags.Object) {
|
|
608
|
+
visitedTypes.add(typeString);
|
|
609
|
+
const properties = extractTypeProperties(checker, type, visitedTypes);
|
|
610
|
+
if (Object.keys(properties).length > 0) {
|
|
611
|
+
return { type: 'object', properties };
|
|
612
|
+
}
|
|
613
|
+
return { type: 'object' };
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
// Fallback
|
|
617
|
+
return resolveTypeToProperties(checker, typeString, visitedTypes);
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
/**
|
|
621
|
+
* Resolves a union type object to a property schema
|
|
622
|
+
* Handles string literal unions and optional types
|
|
623
|
+
* @param {Object} checker - TypeScript type checker
|
|
624
|
+
* @param {Object} type - Union type object
|
|
625
|
+
* @param {Set} visitedTypes - Set of visited types
|
|
626
|
+
* @returns {Object} Property schema
|
|
627
|
+
*/
|
|
628
|
+
function resolveUnionTypeObject(checker, type, visitedTypes) {
|
|
629
|
+
const types = type.types || [];
|
|
630
|
+
|
|
631
|
+
// Filter out undefined and null
|
|
632
|
+
const meaningfulTypes = types.filter(t => {
|
|
633
|
+
const str = checker.typeToString(t);
|
|
634
|
+
return str !== 'undefined' && str !== 'null';
|
|
635
|
+
});
|
|
636
|
+
|
|
637
|
+
if (meaningfulTypes.length === 0) {
|
|
638
|
+
return { type: 'null' };
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
// Check if all remaining types are string literals -> treat as enum
|
|
642
|
+
const allStringLiterals = meaningfulTypes.every(t => t.isStringLiteral?.());
|
|
643
|
+
if (allStringLiterals) {
|
|
644
|
+
const values = meaningfulTypes.map(t => getStringLiteralValue(t, checker));
|
|
645
|
+
return { type: 'enum', values };
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
// Check if all remaining types are number literals -> treat as enum
|
|
649
|
+
const allNumberLiterals = meaningfulTypes.every(t => t.isNumberLiteral?.());
|
|
650
|
+
if (allNumberLiterals) {
|
|
651
|
+
const values = meaningfulTypes.map(t => getNumberLiteralValue(t, checker));
|
|
652
|
+
return { type: 'enum', values };
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
// If only one meaningful type remains, resolve it
|
|
656
|
+
if (meaningfulTypes.length === 1) {
|
|
657
|
+
return resolveTypeObjectToSchema(checker, meaningfulTypes[0], visitedTypes);
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
// Multiple complex types - try to find the most specific one
|
|
661
|
+
// Prefer object types
|
|
662
|
+
for (const t of meaningfulTypes) {
|
|
663
|
+
if (t.flags & ts.TypeFlags.Object) {
|
|
664
|
+
return resolveTypeObjectToSchema(checker, t, visitedTypes);
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
// Fallback to type string
|
|
669
|
+
const typeString = checker.typeToString(type);
|
|
670
|
+
return { type: typeString };
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
/**
|
|
674
|
+
* Gets the actual string value from a string literal type
|
|
675
|
+
* Handles both regular string literals and enum member string literals
|
|
676
|
+
* @param {Object} type - TypeScript type
|
|
677
|
+
* @param {Object} checker - TypeScript type checker
|
|
678
|
+
* @returns {string} The actual string value
|
|
679
|
+
*/
|
|
680
|
+
function getStringLiteralValue(type, checker) {
|
|
681
|
+
// Check if this is an enum member - get the actual value from the initializer
|
|
682
|
+
if (type.symbol && (type.symbol.flags & ts.SymbolFlags.EnumMember)) {
|
|
683
|
+
const valueDecl = type.symbol.valueDeclaration;
|
|
684
|
+
if (valueDecl && ts.isEnumMember(valueDecl) && valueDecl.initializer) {
|
|
685
|
+
if (ts.isStringLiteral(valueDecl.initializer)) {
|
|
686
|
+
return valueDecl.initializer.text;
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
// For regular string literals, remove quotes from the type string
|
|
692
|
+
const str = checker.typeToString(type);
|
|
693
|
+
return str.replace(/^["']|["']$/g, '');
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
/**
|
|
697
|
+
* Gets the actual number value from a number literal type
|
|
698
|
+
* @param {Object} type - TypeScript type
|
|
699
|
+
* @param {Object} checker - TypeScript type checker
|
|
700
|
+
* @returns {number} The actual number value
|
|
701
|
+
*/
|
|
702
|
+
function getNumberLiteralValue(type, checker) {
|
|
703
|
+
// Check if this is an enum member - get the actual value from the initializer
|
|
704
|
+
if (type.symbol && (type.symbol.flags & ts.SymbolFlags.EnumMember)) {
|
|
705
|
+
const valueDecl = type.symbol.valueDeclaration;
|
|
706
|
+
if (valueDecl && ts.isEnumMember(valueDecl) && valueDecl.initializer) {
|
|
707
|
+
if (ts.isNumericLiteral(valueDecl.initializer)) {
|
|
708
|
+
return Number(valueDecl.initializer.text);
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
return Number(checker.typeToString(type));
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
/**
|
|
717
|
+
* Extracts properties from a TypeScript type object
|
|
718
|
+
* @param {Object} checker - TypeScript type checker
|
|
719
|
+
* @param {Object} type - TypeScript Type object
|
|
720
|
+
* @param {Set} visitedTypes - Set of visited types
|
|
721
|
+
* @returns {Object} Properties map
|
|
722
|
+
*/
|
|
723
|
+
function extractTypeProperties(checker, type, visitedTypes) {
|
|
724
|
+
const properties = {};
|
|
725
|
+
|
|
726
|
+
try {
|
|
727
|
+
const members = checker.getPropertiesOfType(type);
|
|
728
|
+
|
|
729
|
+
for (const member of members) {
|
|
730
|
+
const name = member.name;
|
|
731
|
+
|
|
732
|
+
// Skip functions and common built-in methods
|
|
733
|
+
if (STRING_PROTOTYPE_METHODS.has(name) || name.startsWith('__@')) {
|
|
734
|
+
continue;
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
try {
|
|
738
|
+
const memberType = checker.getTypeOfSymbolAtLocation(member, member.valueDeclaration);
|
|
739
|
+
const memberTypeString = checker.typeToString(memberType);
|
|
740
|
+
|
|
741
|
+
// Skip function types
|
|
742
|
+
if (memberTypeString.includes('=>') || memberTypeString.startsWith('(')) {
|
|
743
|
+
continue;
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
// Recursively resolve the member type
|
|
747
|
+
const resolved = resolveTypeObjectToSchema(checker, memberType, visitedTypes);
|
|
748
|
+
properties[name] = resolved;
|
|
749
|
+
} catch (e) {
|
|
750
|
+
properties[name] = { type: 'any' };
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
} catch (e) {
|
|
754
|
+
// Ignore errors
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
return properties;
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
/**
|
|
761
|
+
* Set of string prototype methods to filter out
|
|
762
|
+
*/
|
|
763
|
+
const STRING_PROTOTYPE_METHODS = new Set([
|
|
764
|
+
'toString', 'charAt', 'charCodeAt', 'concat', 'indexOf', 'lastIndexOf',
|
|
765
|
+
'localeCompare', 'match', 'replace', 'search', 'slice', 'split',
|
|
766
|
+
'substring', 'toLowerCase', 'toLocaleLowerCase', 'toUpperCase',
|
|
767
|
+
'toLocaleUpperCase', 'trim', 'length', 'substr', 'valueOf',
|
|
768
|
+
'codePointAt', 'includes', 'endsWith', 'normalize', 'repeat',
|
|
769
|
+
'startsWith', 'anchor', 'big', 'blink', 'bold', 'fixed',
|
|
770
|
+
'fontcolor', 'fontsize', 'italics', 'link', 'small', 'strike',
|
|
771
|
+
'sub', 'sup', 'padStart', 'padEnd', 'trimEnd', 'trimStart',
|
|
772
|
+
'trimLeft', 'trimRight', 'matchAll', 'replaceAll', 'at',
|
|
773
|
+
'isWellFormed', 'toWellFormed'
|
|
774
|
+
]);
|
|
775
|
+
|
|
201
776
|
module.exports = {
|
|
202
777
|
resolveIdentifierToInitializer,
|
|
203
778
|
getTypeOfNode,
|
|
204
779
|
resolveTypeToProperties,
|
|
205
780
|
isCustomType,
|
|
206
781
|
getBasicTypeOfArrayElement,
|
|
207
|
-
isReactHookCall
|
|
782
|
+
isReactHookCall,
|
|
783
|
+
isEnumType,
|
|
784
|
+
getEnumValues,
|
|
785
|
+
resolveTypeObjectToSchema,
|
|
786
|
+
extractTypeProperties
|
|
208
787
|
};
|