@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.
@@ -4,11 +4,15 @@
4
4
  */
5
5
 
6
6
  const ts = require('typescript');
7
- const {
8
- getTypeOfNode,
9
- resolveTypeToProperties,
7
+ const {
8
+ getTypeOfNode,
9
+ resolveTypeToProperties,
10
10
  getBasicTypeOfArrayElement,
11
- isCustomType
11
+ isCustomType,
12
+ isEnumType,
13
+ getEnumValues,
14
+ resolveTypeObjectToSchema,
15
+ extractTypeProperties
12
16
  } = require('../utils/type-resolver');
13
17
 
14
18
  /**
@@ -40,7 +44,7 @@ function extractProperties(checker, node) {
40
44
  Object.assign(properties, spreadProperties);
41
45
  continue;
42
46
  }
43
-
47
+
44
48
  const key = getPropertyKey(prop);
45
49
  if (!key) continue;
46
50
 
@@ -66,16 +70,16 @@ function getPropertyKey(prop) {
66
70
  }
67
71
  return null;
68
72
  }
69
-
73
+
70
74
  // Regular property with name
71
75
  if (ts.isIdentifier(prop.name)) {
72
76
  return prop.name.escapedText;
73
77
  }
74
-
78
+
75
79
  if (ts.isStringLiteral(prop.name)) {
76
80
  return prop.name.text;
77
81
  }
78
-
82
+
79
83
  return null;
80
84
  }
81
85
 
@@ -90,25 +94,25 @@ function extractPropertySchema(checker, prop) {
90
94
  if (ts.isShorthandPropertyAssignment(prop)) {
91
95
  return extractShorthandPropertySchema(checker, prop);
92
96
  }
93
-
97
+
94
98
  // Handle property assignments with initializers
95
99
  if (ts.isPropertyAssignment(prop)) {
96
100
  if (prop.initializer) {
97
101
  return extractValueSchema(checker, prop.initializer);
98
102
  }
99
-
103
+
100
104
  // Property with type annotation but no initializer
101
105
  if (prop.type) {
102
106
  const typeString = checker.typeToString(checker.getTypeFromTypeNode(prop.type));
103
107
  return resolveTypeSchema(checker, typeString);
104
108
  }
105
109
  }
106
-
110
+
107
111
  // Handle method declarations
108
112
  if (ts.isMethodDeclaration(prop)) {
109
113
  return { type: 'function' };
110
114
  }
111
-
115
+
112
116
  return null;
113
117
  }
114
118
 
@@ -155,30 +159,11 @@ function extractShorthandPropertySchema(checker, prop) {
155
159
  return { type: 'any' };
156
160
  }
157
161
  }
158
-
162
+
159
163
  const propType = checker.getTypeAtLocation(prop.name);
160
- const typeString = checker.typeToString(propType);
161
-
162
- // Handle array types
163
- if (isArrayType(typeString)) {
164
- return extractArrayTypeSchema(checker, propType, typeString);
165
- }
166
-
167
- // Handle other types
168
- const resolvedType = resolveTypeToProperties(checker, typeString);
169
-
170
- // If it's an unresolved custom type, try to extract interface properties
171
- if (resolvedType.__unresolved) {
172
- const interfaceProps = extractInterfaceProperties(checker, propType);
173
- if (Object.keys(interfaceProps).length > 0) {
174
- return {
175
- type: 'object',
176
- properties: interfaceProps
177
- };
178
- }
179
- }
180
-
181
- return resolvedType;
164
+
165
+ // Use the type object resolver for better accuracy
166
+ return resolveTypeObjectToSchema(checker, propType);
182
167
  }
183
168
 
184
169
  /**
@@ -188,33 +173,29 @@ function extractShorthandPropertySchema(checker, prop) {
188
173
  * @returns {PropertySchema}
189
174
  */
190
175
  function extractValueSchema(checker, valueNode) {
191
- // Object literal
176
+ // Object literal - extract inline properties
192
177
  if (ts.isObjectLiteralExpression(valueNode)) {
193
178
  return {
194
179
  type: 'object',
195
180
  properties: extractProperties(checker, valueNode)
196
181
  };
197
182
  }
198
-
183
+
199
184
  // Array literal
200
185
  if (ts.isArrayLiteralExpression(valueNode)) {
201
186
  return extractArrayLiteralSchema(checker, valueNode);
202
187
  }
203
-
204
- // Identifier (variable reference)
205
- if (ts.isIdentifier(valueNode)) {
206
- return extractIdentifierSchema(checker, valueNode);
207
- }
208
-
188
+
209
189
  // Literal values
210
190
  const literalType = getLiteralType(valueNode);
211
191
  if (literalType) {
212
192
  return { type: literalType };
213
193
  }
214
-
215
- // For other expressions, get the type from TypeChecker
216
- const typeString = getTypeOfNode(checker, valueNode);
217
- return resolveTypeSchema(checker, typeString);
194
+
195
+ // For all other expressions (identifiers, property access, etc.),
196
+ // use the type object resolver for accurate type resolution
197
+ const valueType = checker.getTypeAtLocation(valueNode);
198
+ return resolveTypeObjectToSchema(checker, valueType);
218
199
  }
219
200
 
220
201
  /**
@@ -230,17 +211,17 @@ function extractArrayLiteralSchema(checker, node) {
230
211
  items: { type: 'any' }
231
212
  };
232
213
  }
233
-
214
+
234
215
  // Check types of all elements
235
216
  const elementTypes = new Set();
236
217
  for (const element of node.elements) {
237
218
  const elemType = getBasicTypeOfArrayElement(checker, element);
238
219
  elementTypes.add(elemType);
239
220
  }
240
-
221
+
241
222
  // If all elements are the same type, use that type
242
223
  const itemType = elementTypes.size === 1 ? Array.from(elementTypes)[0] : 'any';
243
-
224
+
244
225
  return {
245
226
  type: 'array',
246
227
  items: { type: itemType }
@@ -255,28 +236,9 @@ function extractArrayLiteralSchema(checker, node) {
255
236
  */
256
237
  function extractIdentifierSchema(checker, identifier) {
257
238
  const identifierType = checker.getTypeAtLocation(identifier);
258
- const typeString = checker.typeToString(identifierType);
259
-
260
- // Handle array types
261
- if (isArrayType(typeString)) {
262
- return extractArrayTypeSchema(checker, identifierType, typeString);
263
- }
264
-
265
- // Handle other types
266
- const resolvedType = resolveTypeToProperties(checker, typeString);
267
-
268
- // If it's an unresolved custom type, try to extract interface properties
269
- if (resolvedType.__unresolved) {
270
- const interfaceProps = extractInterfaceProperties(checker, identifierType);
271
- if (Object.keys(interfaceProps).length > 0) {
272
- return {
273
- type: 'object',
274
- properties: interfaceProps
275
- };
276
- }
277
- }
278
-
279
- return resolvedType;
239
+
240
+ // Use the new type object resolver for better accuracy
241
+ return resolveTypeObjectToSchema(checker, identifierType);
280
242
  }
281
243
 
282
244
  /**
@@ -288,7 +250,7 @@ function extractIdentifierSchema(checker, identifier) {
288
250
  */
289
251
  function extractArrayTypeSchema(checker, type, typeString) {
290
252
  let elementType = null;
291
-
253
+
292
254
  // Try to get type arguments for generic types
293
255
  if (type.target && type.typeArguments && type.typeArguments.length > 0) {
294
256
  elementType = type.typeArguments[0];
@@ -302,7 +264,7 @@ function extractArrayTypeSchema(checker, type, typeString) {
302
264
  // Indexed access failed
303
265
  }
304
266
  }
305
-
267
+
306
268
  if (elementType) {
307
269
  const elementInterfaceProps = extractInterfaceProperties(checker, elementType);
308
270
  if (Object.keys(elementInterfaceProps).length > 0) {
@@ -327,7 +289,7 @@ function extractArrayTypeSchema(checker, type, typeString) {
327
289
  };
328
290
  }
329
291
  }
330
-
292
+
331
293
  return {
332
294
  type: 'array',
333
295
  items: { type: 'any' }
@@ -342,12 +304,12 @@ function extractArrayTypeSchema(checker, type, typeString) {
342
304
  */
343
305
  function resolveTypeSchema(checker, typeString) {
344
306
  const resolvedType = resolveTypeToProperties(checker, typeString);
345
-
307
+
346
308
  // Clean up any unresolved markers for simple types
347
309
  if (resolvedType.__unresolved) {
348
310
  delete resolvedType.__unresolved;
349
311
  }
350
-
312
+
351
313
  return resolvedType;
352
314
  }
353
315
 
@@ -372,8 +334,8 @@ function getLiteralType(node) {
372
334
  * @returns {boolean}
373
335
  */
374
336
  function isArrayType(typeString) {
375
- return typeString.includes('[]') ||
376
- typeString.startsWith('Array<') ||
337
+ return typeString.includes('[]') ||
338
+ typeString.startsWith('Array<') ||
377
339
  typeString.startsWith('ReadonlyArray<') ||
378
340
  typeString.startsWith('readonly ');
379
341
  }
@@ -388,13 +350,13 @@ function extractSpreadProperties(checker, spreadNode) {
388
350
  if (!spreadNode.expression) {
389
351
  return {};
390
352
  }
391
-
353
+
392
354
  // If the spread is an identifier, resolve it to its declaration
393
355
  if (ts.isIdentifier(spreadNode.expression)) {
394
356
  const symbol = checker.getSymbolAtLocation(spreadNode.expression);
395
357
  if (symbol && symbol.declarations && symbol.declarations.length > 0) {
396
358
  const declaration = symbol.declarations[0];
397
-
359
+
398
360
  // If it's a variable declaration with an object literal initializer
399
361
  if (ts.isVariableDeclaration(declaration) && declaration.initializer) {
400
362
  if (ts.isObjectLiteralExpression(declaration.initializer)) {
@@ -403,17 +365,17 @@ function extractSpreadProperties(checker, spreadNode) {
403
365
  }
404
366
  }
405
367
  }
406
-
368
+
407
369
  // Fallback to the original identifier schema extraction
408
370
  const identifierSchema = extractIdentifierSchema(checker, spreadNode.expression);
409
371
  return identifierSchema.properties || {};
410
372
  }
411
-
373
+
412
374
  // If the spread is an object literal, extract its properties
413
375
  if (ts.isObjectLiteralExpression(spreadNode.expression)) {
414
376
  return extractProperties(checker, spreadNode.expression);
415
377
  }
416
-
378
+
417
379
  // For other expressions, try to get the type and extract properties from it
418
380
  try {
419
381
  const spreadType = checker.getTypeAtLocation(spreadNode.expression);
@@ -432,20 +394,65 @@ function extractSpreadProperties(checker, spreadNode) {
432
394
  function extractInterfaceProperties(checker, type) {
433
395
  const properties = {};
434
396
  const typeSymbol = type.getSymbol();
435
-
397
+
436
398
  if (!typeSymbol) return properties;
437
-
399
+
400
+ // Check if this is an enum type - don't expand enum string methods
401
+ const typeString = checker.typeToString(type);
402
+ if (isEnumType(checker, typeString)) {
403
+ // Return empty - the caller should handle enum types specially
404
+ return properties;
405
+ }
406
+
407
+ // Check if this looks like a string primitive with methods - skip it
408
+ if (isStringPrototype(type, checker)) {
409
+ return properties;
410
+ }
411
+
438
412
  // Get all properties of the type
439
413
  const members = checker.getPropertiesOfType(type);
440
-
441
- for (const member of members) {
414
+
415
+ // Filter out built-in methods (string prototype methods, etc.)
416
+ const userDefinedMembers = members.filter(member => {
417
+ const name = member.name;
418
+ // Skip common built-in method names
419
+ if (STRING_PROTOTYPE_METHODS.has(name)) {
420
+ return false;
421
+ }
422
+ // Skip symbols
423
+ if (name.startsWith('__@')) {
424
+ return false;
425
+ }
426
+ return true;
427
+ });
428
+
429
+ for (const member of userDefinedMembers) {
442
430
  try {
443
431
  const memberType = checker.getTypeOfSymbolAtLocation(member, member.valueDeclaration);
444
432
  const memberTypeString = checker.typeToString(memberType);
445
-
433
+
434
+ // Skip function types
435
+ if (memberTypeString.includes('=>') || memberTypeString.startsWith('(')) {
436
+ continue;
437
+ }
438
+
439
+ // Check if member type is an enum
440
+ if (isEnumType(checker, memberTypeString, memberType)) {
441
+ const enumValues = getEnumValues(checker, memberTypeString, memberType);
442
+ if (enumValues && enumValues.length > 0) {
443
+ properties[member.name] = {
444
+ type: 'enum',
445
+ values: enumValues
446
+ };
447
+ } else {
448
+ properties[member.name] = { type: 'string' };
449
+ }
450
+ continue;
451
+ }
452
+
446
453
  // Recursively resolve the member type
447
454
  const resolvedType = resolveTypeToProperties(checker, memberTypeString);
448
-
455
+
449
456
  // If it's an unresolved object type, try to extract its properties
450
457
  if (resolvedType.__unresolved) {
451
458
  const nestedProperties = extractInterfaceProperties(checker, memberType);
@@ -470,10 +477,41 @@ function extractInterfaceProperties(checker, type) {
470
477
  properties[member.name] = { type: 'any' };
471
478
  }
472
479
  }
473
-
480
+
474
481
  return properties;
475
482
  }
476
483
 
484
+ /**
485
+ * Set of common string prototype method names to filter out
486
+ */
487
+ const STRING_PROTOTYPE_METHODS = new Set([
488
+ 'toString', 'charAt', 'charCodeAt', 'concat', 'indexOf', 'lastIndexOf',
489
+ 'localeCompare', 'match', 'replace', 'search', 'slice', 'split',
490
+ 'substring', 'toLowerCase', 'toLocaleLowerCase', 'toUpperCase',
491
+ 'toLocaleUpperCase', 'trim', 'length', 'substr', 'valueOf',
492
+ 'codePointAt', 'includes', 'endsWith', 'normalize', 'repeat',
493
+ 'startsWith', 'anchor', 'big', 'blink', 'bold', 'fixed',
494
+ 'fontcolor', 'fontsize', 'italics', 'link', 'small', 'strike',
495
+ 'sub', 'sup', 'padStart', 'padEnd', 'trimEnd', 'trimStart',
496
+ 'trimLeft', 'trimRight', 'matchAll', 'replaceAll', 'at',
497
+ 'isWellFormed', 'toWellFormed'
498
+ ]);
499
+
500
+ /**
501
+ * Checks if a type is a string primitive that would have prototype methods
502
+ * @param {Object} type - TypeScript Type object
503
+ * @param {Object} checker - TypeScript type checker
504
+ * @returns {boolean}
505
+ */
506
+ function isStringPrototype(type, checker) {
507
+ if (!type) return false;
508
+ const members = checker.getPropertiesOfType(type);
509
+ // If the type has common string methods, it's likely a string
510
+ const stringMethodCount = members.filter(m => STRING_PROTOTYPE_METHODS.has(m.name)).length;
511
+ // If more than half of the members are string methods, treat as string
512
+ return stringMethodCount > 10 && stringMethodCount > members.length / 2;
513
+ }
514
+
477
515
  module.exports = {
478
516
  extractProperties,
479
517
  extractInterfaceProperties
@@ -168,16 +168,34 @@ function findTrackingEvents(sourceFile, checker, filePath, customConfigs = []) {
168
168
  const events = [];
169
169
 
170
170
  /**
171
- * Tests if a CallExpression matches a custom function name
171
+ * Tests if a CallExpression matches a custom function configuration
172
172
  * @param {Object} callNode - The call expression node
173
- * @param {string} functionName - Function name to match
173
+ * @param {Object} customConfig - Custom function configuration object
174
174
  * @returns {boolean} True if matches
175
175
  */
176
- function matchesCustomFunction(callNode, functionName) {
177
- if (!functionName || !callNode.expression) {
176
+ function matchesCustomFunction(callNode, customConfig) {
177
+ if (!customConfig || !callNode.expression) {
178
178
  return false;
179
179
  }
180
-
180
+
181
+ // Handle method-as-event pattern
182
+ if (customConfig.isMethodAsEvent && customConfig.objectName) {
183
+ if (!ts.isPropertyAccessExpression(callNode.expression)) {
184
+ return false;
185
+ }
186
+ const objectExpr = callNode.expression.expression;
187
+ if (!ts.isIdentifier(objectExpr)) {
188
+ return false;
189
+ }
190
+ return objectExpr.escapedText === customConfig.objectName;
191
+ }
192
+
193
+ // Handle standard custom function pattern
194
+ const functionName = customConfig.functionName;
195
+ if (!functionName) {
196
+ return false;
197
+ }
198
+
181
199
  try {
182
200
  return callNode.expression.getText() === functionName;
183
201
  } catch {
@@ -197,7 +215,7 @@ function findTrackingEvents(sourceFile, checker, filePath, customConfigs = []) {
197
215
  // Check for custom function matches
198
216
  if (Array.isArray(customConfigs) && customConfigs.length > 0) {
199
217
  for (const config of customConfigs) {
200
- if (config && matchesCustomFunction(node, config.functionName)) {
218
+ if (config && matchesCustomFunction(node, config)) {
201
219
  matchedCustomConfig = config;
202
220
  break;
203
221
  }
@@ -211,7 +229,7 @@ function findTrackingEvents(sourceFile, checker, filePath, customConfigs = []) {
211
229
  filePath,
212
230
  matchedCustomConfig
213
231
  );
214
-
232
+
215
233
  if (event) {
216
234
  events.push(event);
217
235
  }
@@ -238,7 +256,8 @@ function findTrackingEvents(sourceFile, checker, filePath, customConfigs = []) {
238
256
  */
239
257
  function extractTrackingEvent(node, sourceFile, checker, filePath, customConfig) {
240
258
  // Detect the analytics source
241
- const source = detectAnalyticsSource(node, customConfig?.functionName);
259
+ // Pass the full customConfig object (not just functionName) to support method-as-event patterns
260
+ const source = detectAnalyticsSource(node, customConfig || null);
242
261
  if (source === 'unknown') {
243
262
  return null;
244
263
  }