@flisk/analyze-tracking 0.7.1 → 0.7.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/README.md +35 -61
- package/package.json +16 -3
- package/src/analyze/analyzeJsFile.js +20 -5
- package/src/analyze/analyzePythonFile.js +1 -0
- package/src/analyze/analyzeRubyFile.js +20 -10
- package/src/analyze/analyzeTsFile.js +138 -15
- package/src/analyze/helpers.js +462 -23
- package/src/analyze/index.js +0 -1
- package/src/analyze/pythonTrackingAnalyzer.py +149 -47
- package/.github/workflows/npm-publish.yml +0 -33
- package/.github/workflows/pr-check.yml +0 -17
- package/jest.config.js +0 -7
- package/tests/detectSource.test.js +0 -20
- package/tests/extractProperties.test.js +0 -109
- package/tests/findWrappingFunction.test.js +0 -30
package/src/analyze/helpers.js
CHANGED
|
@@ -13,16 +13,17 @@ function detectSourceJs(node, customFunction) {
|
|
|
13
13
|
|
|
14
14
|
if (objectName === 'analytics' && methodName === 'track') return 'segment';
|
|
15
15
|
if (objectName === 'mixpanel' && methodName === 'track') return 'mixpanel';
|
|
16
|
-
if (objectName === 'amplitude' && methodName === '
|
|
16
|
+
if (objectName === 'amplitude' && methodName === 'track') return 'amplitude';
|
|
17
17
|
if (objectName === 'rudderanalytics' && methodName === 'track') return 'rudderstack';
|
|
18
|
-
if (objectName === 'mParticle' && methodName === 'logEvent') return 'mparticle';
|
|
18
|
+
if ((objectName === 'mParticle' || objectName === 'mparticle') && methodName === 'logEvent') return 'mparticle';
|
|
19
19
|
if (objectName === 'posthog' && methodName === 'capture') return 'posthog';
|
|
20
20
|
if (objectName === 'pendo' && methodName === 'track') return 'pendo';
|
|
21
21
|
if (objectName === 'heap' && methodName === 'track') return 'heap';
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
22
|
+
|
|
23
|
+
// Check for Snowplow pattern: tracker.track(...)
|
|
24
|
+
if (objectName === 'tracker' && methodName === 'track') {
|
|
25
|
+
return 'snowplow';
|
|
26
|
+
}
|
|
26
27
|
}
|
|
27
28
|
|
|
28
29
|
if (node.callee.type === 'Identifier' && node.callee.name === customFunction) {
|
|
@@ -45,18 +46,19 @@ function detectSourceTs(node, customFunction) {
|
|
|
45
46
|
|
|
46
47
|
if (objectName === 'analytics' && methodName === 'track') return 'segment';
|
|
47
48
|
if (objectName === 'mixpanel' && methodName === 'track') return 'mixpanel';
|
|
48
|
-
if (objectName === 'amplitude' && methodName === '
|
|
49
|
+
if (objectName === 'amplitude' && methodName === 'track') return 'amplitude';
|
|
49
50
|
if (objectName === 'rudderanalytics' && methodName === 'track') return 'rudderstack';
|
|
50
|
-
if (objectName === 'mParticle' && methodName === 'logEvent') return 'mparticle';
|
|
51
|
+
if ((objectName === 'mParticle' || objectName === 'mparticle') && methodName === 'logEvent') return 'mparticle';
|
|
51
52
|
if (objectName === 'posthog' && methodName === 'capture') return 'posthog';
|
|
52
53
|
if (objectName === 'pendo' && methodName === 'track') return 'pendo';
|
|
53
54
|
if (objectName === 'heap' && methodName === 'track') return 'heap';
|
|
55
|
+
|
|
56
|
+
// Check for Snowplow pattern: tracker.track(...)
|
|
57
|
+
if (objectName === 'tracker' && methodName === 'track') {
|
|
58
|
+
return 'snowplow';
|
|
59
|
+
}
|
|
54
60
|
}
|
|
55
61
|
|
|
56
|
-
if (ts.isIdentifier(node.expression) && node.expression.escapedText === 'snowplow') {
|
|
57
|
-
return 'snowplow';
|
|
58
|
-
}
|
|
59
|
-
|
|
60
62
|
if (ts.isIdentifier(node.expression) && node.expression.escapedText === customFunction) {
|
|
61
63
|
return 'custom';
|
|
62
64
|
}
|
|
@@ -94,6 +96,11 @@ function findWrappingFunctionJs(node, ancestors) {
|
|
|
94
96
|
return current.id ? current.id.name : 'anonymous';
|
|
95
97
|
}
|
|
96
98
|
|
|
99
|
+
// Handle class methods
|
|
100
|
+
if (current.type === 'MethodDefinition') {
|
|
101
|
+
return current.key.name || 'anonymous';
|
|
102
|
+
}
|
|
103
|
+
|
|
97
104
|
// Handle exported variable/function (e.g., export const myFunc = () => {})
|
|
98
105
|
if (current.type === 'ExportNamedDeclaration' && current.declaration) {
|
|
99
106
|
const declaration = current.declaration.declarations ? current.declaration.declarations[0] : null;
|
|
@@ -116,13 +123,47 @@ function extractJsProperties(node) {
|
|
|
116
123
|
node.properties.forEach((prop) => {
|
|
117
124
|
const key = prop.key?.name || prop.key?.value;
|
|
118
125
|
if (key) {
|
|
119
|
-
let valueType = typeof prop.value.value;
|
|
120
126
|
if (prop.value.type === 'ObjectExpression') {
|
|
121
127
|
properties[key] = {
|
|
122
128
|
type: 'object',
|
|
123
129
|
properties: extractJsProperties(prop.value),
|
|
124
130
|
};
|
|
131
|
+
} else if (prop.value.type === 'ArrayExpression') {
|
|
132
|
+
// Handle arrays - analyze elements to determine item type
|
|
133
|
+
let itemType = 'any';
|
|
134
|
+
if (prop.value.elements && prop.value.elements.length > 0) {
|
|
135
|
+
// Check the types of all elements
|
|
136
|
+
const elementTypes = new Set();
|
|
137
|
+
prop.value.elements.forEach(element => {
|
|
138
|
+
if (element) {
|
|
139
|
+
if (element.type === 'Literal') {
|
|
140
|
+
elementTypes.add(typeof element.value);
|
|
141
|
+
} else if (element.type === 'ObjectExpression') {
|
|
142
|
+
elementTypes.add('object');
|
|
143
|
+
} else if (element.type === 'ArrayExpression') {
|
|
144
|
+
elementTypes.add('array');
|
|
145
|
+
} else {
|
|
146
|
+
elementTypes.add('any');
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
// If all elements are the same type, use that type
|
|
152
|
+
if (elementTypes.size === 1) {
|
|
153
|
+
itemType = Array.from(elementTypes)[0];
|
|
154
|
+
} else {
|
|
155
|
+
itemType = 'any';
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
properties[key] = {
|
|
160
|
+
type: 'array',
|
|
161
|
+
items: {
|
|
162
|
+
type: itemType
|
|
163
|
+
}
|
|
164
|
+
};
|
|
125
165
|
} else {
|
|
166
|
+
let valueType = typeof prop.value.value;
|
|
126
167
|
if (valueType === 'undefined') {
|
|
127
168
|
valueType = 'any';
|
|
128
169
|
} else if (valueType === 'object') {
|
|
@@ -147,8 +188,74 @@ function extractTsProperties(checker, node) {
|
|
|
147
188
|
if (ts.isShorthandPropertyAssignment(prop)) {
|
|
148
189
|
const symbol = checker.getSymbolAtLocation(prop.name);
|
|
149
190
|
if (symbol) {
|
|
150
|
-
|
|
151
|
-
|
|
191
|
+
// Get the type of the shorthand property
|
|
192
|
+
const propType = checker.getTypeAtLocation(prop.name);
|
|
193
|
+
const typeString = checker.typeToString(propType);
|
|
194
|
+
|
|
195
|
+
// Check if it's an array type
|
|
196
|
+
if (typeString.includes('[]') || typeString.startsWith('Array<')) {
|
|
197
|
+
// Handle array types
|
|
198
|
+
let elementType = null;
|
|
199
|
+
|
|
200
|
+
// Try to get type arguments for generic types
|
|
201
|
+
if (propType.target && propType.typeArguments && propType.typeArguments.length > 0) {
|
|
202
|
+
elementType = propType.typeArguments[0];
|
|
203
|
+
}
|
|
204
|
+
// Try indexed access for array types
|
|
205
|
+
else {
|
|
206
|
+
try {
|
|
207
|
+
const numberType = checker.getNumberType();
|
|
208
|
+
elementType = checker.getIndexedAccessType(propType, numberType);
|
|
209
|
+
} catch (e) {
|
|
210
|
+
// Indexed access failed
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
if (elementType) {
|
|
215
|
+
const elementInterfaceProps = extractInterfaceProperties(checker, elementType);
|
|
216
|
+
if (Object.keys(elementInterfaceProps).length > 0) {
|
|
217
|
+
properties[key] = {
|
|
218
|
+
type: 'array',
|
|
219
|
+
items: {
|
|
220
|
+
type: 'object',
|
|
221
|
+
properties: elementInterfaceProps
|
|
222
|
+
}
|
|
223
|
+
};
|
|
224
|
+
} else {
|
|
225
|
+
properties[key] = {
|
|
226
|
+
type: 'array',
|
|
227
|
+
items: {
|
|
228
|
+
type: 'object'
|
|
229
|
+
}
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
} else {
|
|
233
|
+
properties[key] = {
|
|
234
|
+
type: 'array',
|
|
235
|
+
items: {
|
|
236
|
+
type: 'any'
|
|
237
|
+
}
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
} else {
|
|
241
|
+
// Not an array, handle as before
|
|
242
|
+
const resolvedType = resolveTypeToProperties(checker, typeString);
|
|
243
|
+
if (resolvedType.__unresolved) {
|
|
244
|
+
// Try to get the actual type and extract properties
|
|
245
|
+
const interfaceProps = extractInterfaceProperties(checker, propType);
|
|
246
|
+
if (Object.keys(interfaceProps).length > 0) {
|
|
247
|
+
properties[key] = {
|
|
248
|
+
type: 'object',
|
|
249
|
+
properties: interfaceProps
|
|
250
|
+
};
|
|
251
|
+
} else {
|
|
252
|
+
properties[key] = resolvedType;
|
|
253
|
+
delete properties[key].__unresolved;
|
|
254
|
+
}
|
|
255
|
+
} else {
|
|
256
|
+
properties[key] = resolvedType;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
152
259
|
}
|
|
153
260
|
} else if (prop.initializer) {
|
|
154
261
|
if (ts.isObjectLiteralExpression(prop.initializer)) {
|
|
@@ -157,12 +264,102 @@ function extractTsProperties(checker, node) {
|
|
|
157
264
|
properties: extractTsProperties(checker, prop.initializer),
|
|
158
265
|
};
|
|
159
266
|
} else if (ts.isArrayLiteralExpression(prop.initializer)) {
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
267
|
+
// For array literals, we need to check the elements
|
|
268
|
+
const elementTypes = new Set();
|
|
269
|
+
|
|
270
|
+
if (prop.initializer.elements.length === 0) {
|
|
271
|
+
// Empty array
|
|
272
|
+
properties[key] = {
|
|
273
|
+
type: 'array',
|
|
274
|
+
items: {
|
|
275
|
+
type: 'any'
|
|
276
|
+
}
|
|
277
|
+
};
|
|
278
|
+
} else {
|
|
279
|
+
// Check types of all elements
|
|
280
|
+
for (const element of prop.initializer.elements) {
|
|
281
|
+
const elemType = getBasicTypeOfArrayElement(checker, element);
|
|
282
|
+
elementTypes.add(elemType);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// If all elements are the same type, use that type; otherwise use 'any'
|
|
286
|
+
const itemType = elementTypes.size === 1 ? Array.from(elementTypes)[0] : 'any';
|
|
287
|
+
|
|
288
|
+
properties[key] = {
|
|
289
|
+
type: 'array',
|
|
290
|
+
items: {
|
|
291
|
+
type: itemType
|
|
292
|
+
}
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
} else if (ts.isIdentifier(prop.initializer)) {
|
|
296
|
+
// Handle identifiers (variable references)
|
|
297
|
+
const identifierType = checker.getTypeAtLocation(prop.initializer);
|
|
298
|
+
const typeString = checker.typeToString(identifierType);
|
|
299
|
+
|
|
300
|
+
// Check if it's an array type
|
|
301
|
+
if (typeString.includes('[]') || typeString.startsWith('Array<')) {
|
|
302
|
+
// Extract element type and check if it's a custom interface
|
|
303
|
+
let elementType = null;
|
|
304
|
+
|
|
305
|
+
// Try to get type arguments for generic types
|
|
306
|
+
if (identifierType.target && identifierType.typeArguments && identifierType.typeArguments.length > 0) {
|
|
307
|
+
elementType = identifierType.typeArguments[0];
|
|
308
|
+
}
|
|
309
|
+
// Try indexed access for array types
|
|
310
|
+
else {
|
|
311
|
+
try {
|
|
312
|
+
const numberType = checker.getNumberType();
|
|
313
|
+
elementType = checker.getIndexedAccessType(identifierType, numberType);
|
|
314
|
+
} catch (e) {
|
|
315
|
+
// Indexed access failed
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
if (elementType) {
|
|
320
|
+
const elementInterfaceProps = extractInterfaceProperties(checker, elementType);
|
|
321
|
+
if (Object.keys(elementInterfaceProps).length > 0) {
|
|
322
|
+
properties[key] = {
|
|
323
|
+
type: 'array',
|
|
324
|
+
items: {
|
|
325
|
+
type: 'object',
|
|
326
|
+
properties: elementInterfaceProps
|
|
327
|
+
}
|
|
328
|
+
};
|
|
329
|
+
} else {
|
|
330
|
+
properties[key] = {
|
|
331
|
+
type: 'array',
|
|
332
|
+
items: {
|
|
333
|
+
type: 'object'
|
|
334
|
+
}
|
|
335
|
+
};
|
|
336
|
+
}
|
|
337
|
+
} else {
|
|
338
|
+
properties[key] = {
|
|
339
|
+
type: 'array',
|
|
340
|
+
items: {
|
|
341
|
+
type: 'any'
|
|
342
|
+
}
|
|
343
|
+
};
|
|
344
|
+
}
|
|
345
|
+
} else {
|
|
346
|
+
// Not an array, resolve normally
|
|
347
|
+
const resolvedType = resolveTypeToProperties(checker, typeString);
|
|
348
|
+
if (resolvedType.__unresolved) {
|
|
349
|
+
const interfaceProps = extractInterfaceProperties(checker, identifierType);
|
|
350
|
+
if (Object.keys(interfaceProps).length > 0) {
|
|
351
|
+
properties[key] = {
|
|
352
|
+
type: 'object',
|
|
353
|
+
properties: interfaceProps
|
|
354
|
+
};
|
|
355
|
+
} else {
|
|
356
|
+
properties[key] = resolvedType;
|
|
357
|
+
delete properties[key].__unresolved;
|
|
358
|
+
}
|
|
359
|
+
} else {
|
|
360
|
+
properties[key] = resolvedType;
|
|
361
|
+
}
|
|
362
|
+
}
|
|
166
363
|
} else {
|
|
167
364
|
// Handle hard-coded values
|
|
168
365
|
switch (prop.initializer.kind) {
|
|
@@ -190,11 +387,70 @@ function extractTsProperties(checker, node) {
|
|
|
190
387
|
valueType = getTypeOfNode(checker, prop.initializer) || 'any';
|
|
191
388
|
}
|
|
192
389
|
|
|
193
|
-
|
|
390
|
+
// Check if this is a custom type that should be expanded
|
|
391
|
+
const resolvedType = resolveTypeToProperties(checker, valueType);
|
|
392
|
+
if (resolvedType.__unresolved) {
|
|
393
|
+
// Try to get the actual type and extract properties
|
|
394
|
+
const propType = checker.getTypeAtLocation(prop.initializer);
|
|
395
|
+
const interfaceProps = extractInterfaceProperties(checker, propType);
|
|
396
|
+
if (Object.keys(interfaceProps).length > 0) {
|
|
397
|
+
properties[key] = {
|
|
398
|
+
type: 'object',
|
|
399
|
+
properties: interfaceProps
|
|
400
|
+
};
|
|
401
|
+
} else {
|
|
402
|
+
properties[key] = resolvedType;
|
|
403
|
+
delete properties[key].__unresolved;
|
|
404
|
+
}
|
|
405
|
+
} else {
|
|
406
|
+
properties[key] = resolvedType;
|
|
407
|
+
}
|
|
194
408
|
}
|
|
195
409
|
} else if (prop.type) {
|
|
196
410
|
valueType = checker.typeToString(checker.getTypeFromTypeNode(prop.type)) || 'any';
|
|
197
|
-
|
|
411
|
+
|
|
412
|
+
// Check if this is a custom type that should be expanded
|
|
413
|
+
const resolvedType = resolveTypeToProperties(checker, valueType);
|
|
414
|
+
|
|
415
|
+
// Special handling for arrays of custom types
|
|
416
|
+
if (resolvedType.type === 'array') {
|
|
417
|
+
const propType = checker.getTypeFromTypeNode(prop.type);
|
|
418
|
+
|
|
419
|
+
// Try multiple approaches to get array element type
|
|
420
|
+
let elementType = null;
|
|
421
|
+
|
|
422
|
+
// First try: Check if it's a generic type reference (Array<T> or ReadonlyArray<T>)
|
|
423
|
+
if (propType.target && propType.typeArguments && propType.typeArguments.length > 0) {
|
|
424
|
+
elementType = propType.typeArguments[0];
|
|
425
|
+
}
|
|
426
|
+
// Second try: For T[] syntax, use indexed access
|
|
427
|
+
else {
|
|
428
|
+
try {
|
|
429
|
+
const numberType = checker.getNumberType();
|
|
430
|
+
elementType = checker.getIndexedAccessType(propType, numberType);
|
|
431
|
+
} catch (e) {
|
|
432
|
+
// Indexed access failed
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
if (elementType) {
|
|
437
|
+
const elementInterfaceProps = extractInterfaceProperties(checker, elementType);
|
|
438
|
+
if (Object.keys(elementInterfaceProps).length > 0) {
|
|
439
|
+
resolvedType.items = {
|
|
440
|
+
type: 'object',
|
|
441
|
+
properties: elementInterfaceProps
|
|
442
|
+
};
|
|
443
|
+
} else {
|
|
444
|
+
// If no properties found but it looks like a custom type, mark as object
|
|
445
|
+
const elementTypeString = checker.typeToString(elementType);
|
|
446
|
+
if (elementTypeString[0] === elementTypeString[0].toUpperCase() && !elementTypeString.includes('<')) {
|
|
447
|
+
resolvedType.items = { type: 'object' };
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
properties[key] = resolvedType;
|
|
198
454
|
}
|
|
199
455
|
}
|
|
200
456
|
|
|
@@ -206,6 +462,185 @@ function getTypeOfNode(checker, node) {
|
|
|
206
462
|
return checker.typeToString(type);
|
|
207
463
|
}
|
|
208
464
|
|
|
465
|
+
function getBasicTypeOfArrayElement(checker, element) {
|
|
466
|
+
if (!element) return 'any';
|
|
467
|
+
|
|
468
|
+
// Check for literal values first
|
|
469
|
+
if (ts.isStringLiteral(element)) {
|
|
470
|
+
return 'string';
|
|
471
|
+
} else if (ts.isNumericLiteral(element)) {
|
|
472
|
+
return 'number';
|
|
473
|
+
} else if (element.kind === ts.SyntaxKind.TrueKeyword || element.kind === ts.SyntaxKind.FalseKeyword) {
|
|
474
|
+
return 'boolean';
|
|
475
|
+
} else if (ts.isObjectLiteralExpression(element)) {
|
|
476
|
+
return 'object';
|
|
477
|
+
} else if (ts.isArrayLiteralExpression(element)) {
|
|
478
|
+
return 'array';
|
|
479
|
+
} else if (element.kind === ts.SyntaxKind.NullKeyword) {
|
|
480
|
+
return 'null';
|
|
481
|
+
} else if (element.kind === ts.SyntaxKind.UndefinedKeyword) {
|
|
482
|
+
return 'undefined';
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
// For identifiers and other expressions, try to get the type
|
|
486
|
+
const typeString = getTypeOfNode(checker, element);
|
|
487
|
+
|
|
488
|
+
// Extract basic type from TypeScript type string
|
|
489
|
+
if (typeString.startsWith('"') || typeString.startsWith("'")) {
|
|
490
|
+
return 'string'; // String literal type
|
|
491
|
+
} else if (!isNaN(Number(typeString))) {
|
|
492
|
+
return 'number'; // Numeric literal type
|
|
493
|
+
} else if (typeString === 'true' || typeString === 'false') {
|
|
494
|
+
return 'boolean'; // Boolean literal type
|
|
495
|
+
} else if (typeString.includes('[]') || typeString.startsWith('Array<')) {
|
|
496
|
+
return 'array';
|
|
497
|
+
} else if (typeString === 'string' || typeString === 'number' || typeString === 'boolean' ||
|
|
498
|
+
typeString === 'object' || typeString === 'null' || typeString === 'undefined') {
|
|
499
|
+
return typeString;
|
|
500
|
+
} else if (typeString[0] === typeString[0].toUpperCase() && !typeString.includes('<')) {
|
|
501
|
+
// This looks like a custom type/interface, return 'object'
|
|
502
|
+
return 'object';
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
return 'any';
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
function resolveIdentifierToInitializer(checker, identifier, sourceFile) {
|
|
509
|
+
try {
|
|
510
|
+
const symbol = checker.getSymbolAtLocation(identifier);
|
|
511
|
+
if (!symbol || !symbol.valueDeclaration) return null;
|
|
512
|
+
|
|
513
|
+
const declaration = symbol.valueDeclaration;
|
|
514
|
+
|
|
515
|
+
// Handle variable declarations
|
|
516
|
+
if (ts.isVariableDeclaration(declaration) && declaration.initializer) {
|
|
517
|
+
return declaration.initializer;
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
// Handle property assignments
|
|
521
|
+
if (ts.isPropertyAssignment(declaration) && declaration.initializer) {
|
|
522
|
+
return declaration.initializer;
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
// Handle parameter with default value
|
|
526
|
+
if (ts.isParameter(declaration) && declaration.initializer) {
|
|
527
|
+
return declaration.initializer;
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
return null;
|
|
531
|
+
} catch (error) {
|
|
532
|
+
return null;
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
function resolveTypeToProperties(checker, typeString, visitedTypes = new Set()) {
|
|
537
|
+
// Prevent infinite recursion for circular references
|
|
538
|
+
if (visitedTypes.has(typeString)) {
|
|
539
|
+
return { type: typeString };
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
// Handle primitive types
|
|
543
|
+
if (['string', 'number', 'boolean', 'any', 'unknown', 'null', 'undefined'].includes(typeString)) {
|
|
544
|
+
return { type: typeString };
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
// Handle array types
|
|
548
|
+
const arrayMatch = typeString.match(/^(.+)\[\]$/) || typeString.match(/^Array<(.+)>$/);
|
|
549
|
+
if (arrayMatch) {
|
|
550
|
+
const elementType = arrayMatch[1].trim();
|
|
551
|
+
visitedTypes.add(typeString);
|
|
552
|
+
const elementProps = resolveTypeToProperties(checker, elementType, visitedTypes);
|
|
553
|
+
return {
|
|
554
|
+
type: 'array',
|
|
555
|
+
items: elementProps
|
|
556
|
+
};
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
// Handle readonly array types
|
|
560
|
+
const readonlyArrayMatch = typeString.match(/^readonly (.+)\[\]$/) || typeString.match(/^ReadonlyArray<(.+)>$/);
|
|
561
|
+
if (readonlyArrayMatch) {
|
|
562
|
+
const elementType = readonlyArrayMatch[1].trim();
|
|
563
|
+
visitedTypes.add(typeString);
|
|
564
|
+
const elementProps = resolveTypeToProperties(checker, elementType, visitedTypes);
|
|
565
|
+
|
|
566
|
+
// If element type is a custom interface/type, we need to find its properties
|
|
567
|
+
if (elementProps.__unresolved && checker) {
|
|
568
|
+
// Try to find the type by name and extract its properties
|
|
569
|
+
// This would require access to the program's type checker context
|
|
570
|
+
// For now, mark it as object type but try to get properties later
|
|
571
|
+
return {
|
|
572
|
+
type: 'array',
|
|
573
|
+
items: {
|
|
574
|
+
type: 'object',
|
|
575
|
+
__needsResolution: elementType
|
|
576
|
+
}
|
|
577
|
+
};
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
return {
|
|
581
|
+
type: 'array',
|
|
582
|
+
items: elementProps
|
|
583
|
+
};
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
// Try to find the type symbol and extract properties
|
|
587
|
+
try {
|
|
588
|
+
// This is a simplified approach - in a real implementation, we'd need access to the actual type node
|
|
589
|
+
// For now, we'll return the type as-is, but mark it as an object if it looks like a custom type
|
|
590
|
+
if (typeString[0] === typeString[0].toUpperCase() && !typeString.includes('<') && !typeString.includes('|') && !typeString.includes('&')) {
|
|
591
|
+
// Looks like a custom type/interface
|
|
592
|
+
return {
|
|
593
|
+
type: 'object',
|
|
594
|
+
__unresolved: typeString // Mark for later resolution
|
|
595
|
+
};
|
|
596
|
+
}
|
|
597
|
+
} catch (error) {
|
|
598
|
+
// Fall through
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
return { type: typeString };
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
function extractInterfaceProperties(checker, type) {
|
|
605
|
+
const properties = {};
|
|
606
|
+
const typeSymbol = type.getSymbol();
|
|
607
|
+
|
|
608
|
+
if (!typeSymbol) return properties;
|
|
609
|
+
|
|
610
|
+
// Get all properties of the type
|
|
611
|
+
const members = checker.getPropertiesOfType(type);
|
|
612
|
+
|
|
613
|
+
for (const member of members) {
|
|
614
|
+
const memberType = checker.getTypeOfSymbolAtLocation(member, member.valueDeclaration);
|
|
615
|
+
const memberTypeString = checker.typeToString(memberType);
|
|
616
|
+
const isOptional = member.flags & ts.SymbolFlags.Optional;
|
|
617
|
+
|
|
618
|
+
// Recursively resolve the member type
|
|
619
|
+
const resolvedType = resolveTypeToProperties(checker, memberTypeString);
|
|
620
|
+
|
|
621
|
+
// If it's an unresolved object type, try to extract its properties
|
|
622
|
+
if (resolvedType.__unresolved) {
|
|
623
|
+
const nestedProperties = extractInterfaceProperties(checker, memberType);
|
|
624
|
+
if (Object.keys(nestedProperties).length > 0) {
|
|
625
|
+
properties[member.name] = {
|
|
626
|
+
type: 'object',
|
|
627
|
+
properties: nestedProperties
|
|
628
|
+
};
|
|
629
|
+
} else {
|
|
630
|
+
properties[member.name] = resolvedType;
|
|
631
|
+
}
|
|
632
|
+
} else {
|
|
633
|
+
properties[member.name] = resolvedType;
|
|
634
|
+
// Clean up any unresolved markers
|
|
635
|
+
if (properties[member.name].__unresolved) {
|
|
636
|
+
delete properties[member.name].__unresolved;
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
return properties;
|
|
642
|
+
}
|
|
643
|
+
|
|
209
644
|
module.exports = {
|
|
210
645
|
detectSourceJs,
|
|
211
646
|
detectSourceTs,
|
|
@@ -214,4 +649,8 @@ module.exports = {
|
|
|
214
649
|
extractJsProperties,
|
|
215
650
|
extractTsProperties,
|
|
216
651
|
getTypeOfNode,
|
|
652
|
+
getBasicTypeOfArrayElement,
|
|
653
|
+
resolveIdentifierToInitializer,
|
|
654
|
+
resolveTypeToProperties,
|
|
655
|
+
extractInterfaceProperties,
|
|
217
656
|
};
|
package/src/analyze/index.js
CHANGED