@flisk/analyze-tracking 0.7.5 → 0.7.6

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@flisk/analyze-tracking",
3
- "version": "0.7.5",
3
+ "version": "0.7.6",
4
4
  "description": "Analyzes tracking code in a project and generates data schemas",
5
5
  "main": "src/index.js",
6
6
  "bin": {
@@ -48,7 +48,10 @@ function isCustomFunction(node, customFunction) {
48
48
  ts.isPropertyAccessExpression(node.expression) ||
49
49
  ts.isCallExpression(node.expression) || // For chained calls like getTracker().track()
50
50
  ts.isElementAccessExpression(node.expression) || // For array/object access like trackers['analytics'].track()
51
- (ts.isPropertyAccessExpression(node.expression?.expression) && ts.isThisExpression(node.expression.expression.expression)); // For class methods like this.analytics.track()
51
+ (node.expression?.expression &&
52
+ ts.isPropertyAccessExpression(node.expression.expression) &&
53
+ node.expression.expression.expression &&
54
+ ts.isThisExpression(node.expression.expression.expression)); // For class methods like this.analytics.track()
52
55
 
53
56
  return canBeCustomFunction && node.expression.getText() === customFunction;
54
57
  }
@@ -50,7 +50,7 @@ function extractGoogleAnalyticsEvent(node, checker, sourceFile) {
50
50
  }
51
51
 
52
52
  // gtag('event', 'event_name', { properties })
53
- const eventName = getStringValue(node.arguments[1]);
53
+ const eventName = getStringValue(node.arguments[1], checker, sourceFile);
54
54
  const propertiesNode = node.arguments[2];
55
55
 
56
56
  return { eventName, propertiesNode };
@@ -79,7 +79,7 @@ function extractSnowplowEvent(node, checker, sourceFile) {
79
79
  const structEventArg = firstArg.arguments[0];
80
80
  if (ts.isObjectLiteralExpression(structEventArg)) {
81
81
  const actionProperty = findPropertyByKey(structEventArg, 'action');
82
- const eventName = actionProperty ? getStringValue(actionProperty.initializer) : null;
82
+ const eventName = actionProperty ? getStringValue(actionProperty.initializer, checker, sourceFile) : null;
83
83
  return { eventName, propertiesNode: structEventArg };
84
84
  }
85
85
  }
@@ -93,7 +93,7 @@ function extractSnowplowEvent(node, checker, sourceFile) {
93
93
  const structEventArg = resolvedNode.arguments[0];
94
94
  if (ts.isObjectLiteralExpression(structEventArg)) {
95
95
  const actionProperty = findPropertyByKey(structEventArg, 'action');
96
- const eventName = actionProperty ? getStringValue(actionProperty.initializer) : null;
96
+ const eventName = actionProperty ? getStringValue(actionProperty.initializer, checker, sourceFile) : null;
97
97
  return { eventName, propertiesNode: structEventArg };
98
98
  }
99
99
  }
@@ -115,7 +115,7 @@ function extractMparticleEvent(node, checker, sourceFile) {
115
115
  }
116
116
 
117
117
  // mParticle.logEvent('event_name', mParticle.EventType.Navigation, { properties })
118
- const eventName = getStringValue(node.arguments[0]);
118
+ const eventName = getStringValue(node.arguments[0], checker, sourceFile);
119
119
  const propertiesNode = node.arguments[2];
120
120
 
121
121
  return { eventName, propertiesNode };
@@ -134,7 +134,7 @@ function extractDefaultEvent(node, checker, sourceFile) {
134
134
  }
135
135
 
136
136
  // provider.track('event_name', { properties })
137
- const eventName = getStringValue(node.arguments[0]);
137
+ const eventName = getStringValue(node.arguments[0], checker, sourceFile);
138
138
  const propertiesNode = node.arguments[1];
139
139
 
140
140
  return { eventName, propertiesNode };
@@ -197,16 +197,121 @@ function processEventData(eventData, source, filePath, line, functionName, check
197
197
  /**
198
198
  * Gets string value from a TypeScript AST node
199
199
  * @param {Object} node - TypeScript AST node
200
+ * @param {Object} checker - TypeScript type checker
201
+ * @param {Object} sourceFile - TypeScript source file
200
202
  * @returns {string|null} String value or null
201
203
  */
202
- function getStringValue(node) {
204
+ function getStringValue(node, checker, sourceFile) {
203
205
  if (!node) return null;
206
+
207
+ // Handle string literals (existing behavior)
204
208
  if (ts.isStringLiteral(node)) {
205
209
  return node.text;
206
210
  }
211
+
212
+ // Handle property access expressions like TRACKING_EVENTS.ECOMMERCE_PURCHASE
213
+ if (ts.isPropertyAccessExpression(node)) {
214
+ return resolvePropertyAccessToString(node, checker, sourceFile);
215
+ }
216
+
217
+ // Handle identifiers that might reference constants
218
+ if (ts.isIdentifier(node)) {
219
+ return resolveIdentifierToString(node, checker, sourceFile);
220
+ }
221
+
207
222
  return null;
208
223
  }
209
224
 
225
+ /**
226
+ * Resolves a property access expression to its string value
227
+ * @param {Object} node - PropertyAccessExpression node
228
+ * @param {Object} checker - TypeScript type checker
229
+ * @param {Object} sourceFile - TypeScript source file
230
+ * @returns {string|null} String value or null
231
+ */
232
+ function resolvePropertyAccessToString(node, checker, sourceFile) {
233
+ try {
234
+ // Get the symbol for the property access
235
+ const symbol = checker.getSymbolAtLocation(node);
236
+ if (!symbol || !symbol.valueDeclaration) {
237
+ return null;
238
+ }
239
+
240
+ // Check if it's a property assignment with a string initializer
241
+ if (ts.isPropertyAssignment(symbol.valueDeclaration) &&
242
+ symbol.valueDeclaration.initializer &&
243
+ ts.isStringLiteral(symbol.valueDeclaration.initializer)) {
244
+ return symbol.valueDeclaration.initializer.text;
245
+ }
246
+
247
+ // Check if it's a variable declaration property
248
+ if (ts.isPropertySignature(symbol.valueDeclaration) ||
249
+ ts.isMethodSignature(symbol.valueDeclaration)) {
250
+ // Try to get the type and see if it's a string literal type
251
+ const type = checker.getTypeAtLocation(node);
252
+ if (type.isStringLiteral && type.isStringLiteral()) {
253
+ return type.value;
254
+ }
255
+ }
256
+
257
+ return null;
258
+ } catch (error) {
259
+ return null;
260
+ }
261
+ }
262
+
263
+ /**
264
+ * Resolves an identifier to its string value
265
+ * @param {Object} node - Identifier node
266
+ * @param {Object} checker - TypeScript type checker
267
+ * @param {Object} sourceFile - TypeScript source file
268
+ * @returns {string|null} String value or null
269
+ */
270
+ function resolveIdentifierToString(node, checker, sourceFile) {
271
+ try {
272
+ const symbol = checker.getSymbolAtLocation(node);
273
+ if (!symbol) {
274
+ return null;
275
+ }
276
+
277
+ // First try to resolve through value declaration
278
+ if (symbol.valueDeclaration) {
279
+ const declaration = symbol.valueDeclaration;
280
+
281
+ // Handle variable declarations with string literal initializers
282
+ if (ts.isVariableDeclaration(declaration) &&
283
+ declaration.initializer &&
284
+ ts.isStringLiteral(declaration.initializer)) {
285
+ return declaration.initializer.text;
286
+ }
287
+
288
+ // Handle const declarations with object literals containing string properties
289
+ if (ts.isVariableDeclaration(declaration) &&
290
+ declaration.initializer &&
291
+ ts.isObjectLiteralExpression(declaration.initializer)) {
292
+ // This case is handled by property access resolution
293
+ return null;
294
+ }
295
+ }
296
+
297
+ // If value declaration doesn't exist or doesn't help, try type resolution
298
+ // This handles imported constants that are resolved through TypeScript's type system
299
+ const type = checker.getTypeOfSymbolAtLocation(symbol, node);
300
+ if (type && type.isStringLiteral && typeof type.isStringLiteral === 'function' && type.isStringLiteral()) {
301
+ return type.value;
302
+ }
303
+
304
+ // Alternative approach for string literal types (different TypeScript versions)
305
+ if (type && type.flags && (type.flags & ts.TypeFlags.StringLiteral)) {
306
+ return type.value;
307
+ }
308
+
309
+ return null;
310
+ } catch (error) {
311
+ return null;
312
+ }
313
+ }
314
+
210
315
  /**
211
316
  * Finds a property by key in an ObjectLiteralExpression
212
317
  * @param {Object} objectNode - ObjectLiteralExpression node
@@ -34,6 +34,13 @@ function extractProperties(checker, node) {
34
34
  const properties = {};
35
35
 
36
36
  for (const prop of node.properties) {
37
+ // Handle spread assignments like {...object}
38
+ if (ts.isSpreadAssignment(prop)) {
39
+ const spreadProperties = extractSpreadProperties(checker, prop);
40
+ Object.assign(properties, spreadProperties);
41
+ continue;
42
+ }
43
+
37
44
  const key = getPropertyKey(prop);
38
45
  if (!key) continue;
39
46
 
@@ -350,7 +357,7 @@ function resolveTypeSchema(checker, typeString) {
350
357
  * @returns {string|null} Literal type or null
351
358
  */
352
359
  function getLiteralType(node) {
353
- if (!node) return null;
360
+ if (!node || typeof node.kind === 'undefined') return null;
354
361
  if (ts.isStringLiteral(node)) return 'string';
355
362
  if (ts.isNumericLiteral(node)) return 'number';
356
363
  if (node.kind === ts.SyntaxKind.TrueKeyword || node.kind === ts.SyntaxKind.FalseKeyword) return 'boolean';
@@ -371,6 +378,51 @@ function isArrayType(typeString) {
371
378
  typeString.startsWith('readonly ');
372
379
  }
373
380
 
381
+ /**
382
+ * Extracts properties from a spread assignment
383
+ * @param {Object} checker - TypeScript type checker
384
+ * @param {Object} spreadNode - SpreadAssignment node
385
+ * @returns {Object.<string, PropertySchema>}
386
+ */
387
+ function extractSpreadProperties(checker, spreadNode) {
388
+ if (!spreadNode.expression) {
389
+ return {};
390
+ }
391
+
392
+ // If the spread is an identifier, resolve it to its declaration
393
+ if (ts.isIdentifier(spreadNode.expression)) {
394
+ const symbol = checker.getSymbolAtLocation(spreadNode.expression);
395
+ if (symbol && symbol.declarations && symbol.declarations.length > 0) {
396
+ const declaration = symbol.declarations[0];
397
+
398
+ // If it's a variable declaration with an object literal initializer
399
+ if (ts.isVariableDeclaration(declaration) && declaration.initializer) {
400
+ if (ts.isObjectLiteralExpression(declaration.initializer)) {
401
+ // Extract properties directly from the object literal
402
+ return extractProperties(checker, declaration.initializer);
403
+ }
404
+ }
405
+ }
406
+
407
+ // Fallback to the original identifier schema extraction
408
+ const identifierSchema = extractIdentifierSchema(checker, spreadNode.expression);
409
+ return identifierSchema.properties || {};
410
+ }
411
+
412
+ // If the spread is an object literal, extract its properties
413
+ if (ts.isObjectLiteralExpression(spreadNode.expression)) {
414
+ return extractProperties(checker, spreadNode.expression);
415
+ }
416
+
417
+ // For other expressions, try to get the type and extract properties from it
418
+ try {
419
+ const spreadType = checker.getTypeAtLocation(spreadNode.expression);
420
+ return extractInterfaceProperties(checker, spreadType);
421
+ } catch (error) {
422
+ return {};
423
+ }
424
+ }
425
+
374
426
  /**
375
427
  * Extracts properties from a TypeScript interface or type
376
428
  * @param {Object} checker - TypeScript type checker
@@ -110,6 +110,16 @@ function findParentFunctionName(node) {
110
110
  }
111
111
  }
112
112
 
113
+ // Property declaration in class: myFunc = () => {}
114
+ if (ts.isPropertyDeclaration(parent) && parent.name) {
115
+ if (ts.isIdentifier(parent.name)) {
116
+ return parent.name.escapedText;
117
+ }
118
+ if (ts.isStringLiteral(parent.name)) {
119
+ return parent.name.text;
120
+ }
121
+ }
122
+
113
123
  // Method property in object literal: { myFunc() {} }
114
124
  if (ts.isMethodDeclaration(parent) && parent.name) {
115
125
  return parent.name.escapedText;
@@ -117,6 +127,7 @@ function findParentFunctionName(node) {
117
127
 
118
128
  // Binary expression assignment: obj.myFunc = () => {}
119
129
  if (ts.isBinaryExpression(parent) &&
130
+ parent.operatorToken &&
120
131
  parent.operatorToken.kind === ts.SyntaxKind.EqualsToken) {
121
132
  if (ts.isPropertyAccessExpression(parent.left)) {
122
133
  return parent.left.name.escapedText;
@@ -144,7 +144,7 @@ function isCustomType(typeString) {
144
144
  * @returns {string} Basic type string
145
145
  */
146
146
  function getBasicTypeOfArrayElement(checker, element) {
147
- if (!element) return 'any';
147
+ if (!element || typeof element.kind === 'undefined') return 'any';
148
148
 
149
149
  // Check for literal values first
150
150
  if (ts.isStringLiteral(element)) {