@flisk/analyze-tracking 0.5.2 → 0.7.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,968 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const { extractGoAST } = require('./go2json');
4
+
5
+ async function analyzeGoFile(filePath, customFunction) {
6
+ try {
7
+ // Read the Go file
8
+ const source = fs.readFileSync(filePath, 'utf8');
9
+
10
+ // Parse the Go file using go2json
11
+ const ast = extractGoAST(source);
12
+
13
+ // Extract tracking events from the AST
14
+ const events = [];
15
+ let currentFunction = 'global';
16
+
17
+ // Walk through the AST
18
+ for (const node of ast) {
19
+ if (node.tag === 'func') {
20
+ currentFunction = node.name;
21
+ // Process the function body
22
+ if (node.body) {
23
+ extractEventsFromBody(node.body, events, filePath, currentFunction, customFunction);
24
+ }
25
+ }
26
+ }
27
+
28
+ // Deduplicate events based on eventName, source, and function
29
+ const uniqueEvents = [];
30
+ const seen = new Set();
31
+
32
+ for (const event of events) {
33
+ // For Amplitude, we want to keep the line number from the struct literal
34
+ // For other sources, we can use any line number since they don't have this issue
35
+ const key = `${event.eventName}:${event.source}:${event.functionName}`;
36
+ if (!seen.has(key)) {
37
+ seen.add(key);
38
+ uniqueEvents.push(event);
39
+ } else {
40
+ // If we've seen this event before and it's Amplitude, check if this is the struct literal version
41
+ const existingEvent = uniqueEvents.find(e =>
42
+ e.eventName === event.eventName &&
43
+ e.source === event.source &&
44
+ e.functionName === event.functionName
45
+ );
46
+
47
+ // If this is Amplitude and the existing event is from the function call (higher line number),
48
+ // replace it with this one (from the struct literal)
49
+ if (event.source === 'amplitude' && existingEvent && existingEvent.line > event.line) {
50
+ const index = uniqueEvents.indexOf(existingEvent);
51
+ uniqueEvents[index] = event;
52
+ }
53
+ }
54
+ }
55
+
56
+ return uniqueEvents;
57
+ } catch (error) {
58
+ console.error(`Error analyzing Go file ${filePath}:`, error.message);
59
+ return [];
60
+ }
61
+ }
62
+
63
+ function extractEventsFromBody(body, events, filePath, functionName, customFunction) {
64
+ for (const stmt of body) {
65
+ if (stmt.tag === 'exec' && stmt.expr) {
66
+ processExpression(stmt.expr, events, filePath, functionName, customFunction);
67
+ } else if (stmt.tag === 'declare' && stmt.value) {
68
+ // Handle variable declarations with tracking calls
69
+ processExpression(stmt.value, events, filePath, functionName, customFunction);
70
+ } else if (stmt.tag === 'assign' && stmt.rhs) {
71
+ // Handle assignments with tracking calls
72
+ processExpression(stmt.rhs, events, filePath, functionName, customFunction);
73
+ } else if (stmt.tag === 'if' && stmt.body) {
74
+ extractEventsFromBody(stmt.body, events, filePath, functionName, customFunction);
75
+ } else if (stmt.tag === 'elseif' && stmt.body) {
76
+ extractEventsFromBody(stmt.body, events, filePath, functionName, customFunction);
77
+ } else if (stmt.tag === 'else' && stmt.body) {
78
+ extractEventsFromBody(stmt.body, events, filePath, functionName, customFunction);
79
+ } else if (stmt.tag === 'for' && stmt.body) {
80
+ extractEventsFromBody(stmt.body, events, filePath, functionName, customFunction);
81
+ } else if (stmt.tag === 'foreach' && stmt.body) {
82
+ extractEventsFromBody(stmt.body, events, filePath, functionName, customFunction);
83
+ } else if (stmt.tag === 'switch' && stmt.cases) {
84
+ for (const caseNode of stmt.cases) {
85
+ if (caseNode.body) {
86
+ extractEventsFromBody(caseNode.body, events, filePath, functionName, customFunction);
87
+ }
88
+ }
89
+ }
90
+ }
91
+ }
92
+
93
+ function processExpression(expr, events, filePath, functionName, customFunction, depth = 0) {
94
+ if (!expr || depth > 20) return; // Prevent infinite recursion with depth limit
95
+
96
+ // Handle array of expressions
97
+ if (Array.isArray(expr)) {
98
+ for (const item of expr) {
99
+ processExpression(item, events, filePath, functionName, customFunction, depth + 1);
100
+ }
101
+ return;
102
+ }
103
+
104
+ // Handle single expression with body
105
+ if (expr.body) {
106
+ for (const item of expr.body) {
107
+ processExpression(item, events, filePath, functionName, customFunction, depth + 1);
108
+ }
109
+ return;
110
+ }
111
+
112
+ // Handle specific node types
113
+ if (expr.tag === 'call') {
114
+ const trackingCall = extractTrackingCall(expr, filePath, functionName, customFunction);
115
+ if (trackingCall) {
116
+ events.push(trackingCall);
117
+ }
118
+
119
+ // Also process call arguments
120
+ if (expr.args) {
121
+ processExpression(expr.args, events, filePath, functionName, customFunction, depth + 1);
122
+ }
123
+ } else if (expr.tag === 'structlit') {
124
+ // Check if this struct literal is a tracking event
125
+ const trackingCall = extractTrackingCall(expr, filePath, functionName, customFunction);
126
+ if (trackingCall) {
127
+ events.push(trackingCall);
128
+ }
129
+
130
+ // Process fields (but don't recurse into field values for tracking structs)
131
+ if (!trackingCall && expr.fields) {
132
+ for (const field of expr.fields) {
133
+ if (field.value) {
134
+ processExpression(field.value, events, filePath, functionName, customFunction, depth + 1);
135
+ }
136
+ }
137
+ }
138
+ }
139
+
140
+ // Process other common properties that might contain expressions
141
+ if (expr.value && expr.tag !== 'structlit') {
142
+ processExpression(expr.value, events, filePath, functionName, customFunction, depth + 1);
143
+ }
144
+ if (expr.lhs) {
145
+ processExpression(expr.lhs, events, filePath, functionName, customFunction, depth + 1);
146
+ }
147
+ if (expr.rhs) {
148
+ processExpression(expr.rhs, events, filePath, functionName, customFunction, depth + 1);
149
+ }
150
+ }
151
+
152
+ function extractTrackingCall(callNode, filePath, functionName, customFunction) {
153
+ const source = detectSource(callNode, customFunction);
154
+ if (!source) return null;
155
+
156
+ const eventName = extractEventName(callNode, source);
157
+ if (!eventName) return null;
158
+
159
+ const properties = extractProperties(callNode, source);
160
+
161
+ // Get line number based on source type
162
+ let line = 0;
163
+ if (source === 'segment' || source === 'posthog') {
164
+ // For Segment and PostHog, we need to get the line number from the struct.struct object
165
+ if (callNode.tag === 'structlit' && callNode.struct && callNode.struct.struct) {
166
+ line = callNode.struct.struct.line || 0;
167
+ }
168
+ } else {
169
+ // For other sources, use the line number from the AST node
170
+ line = callNode.line || 0;
171
+ }
172
+
173
+ return {
174
+ eventName,
175
+ source,
176
+ properties,
177
+ filePath,
178
+ line,
179
+ functionName
180
+ };
181
+ }
182
+
183
+ function detectSource(callNode, customFunction) {
184
+ // Check for struct literals (Segment/Rudderstack/PostHog/Amplitude)
185
+ if (callNode.tag === 'structlit') {
186
+ if (callNode.struct) {
187
+ if (callNode.struct.tag === 'access') {
188
+ const structType = callNode.struct.member;
189
+ const namespace = callNode.struct.struct?.value;
190
+
191
+ // Check for specific struct types with their namespaces
192
+ if (structType === 'Track' && namespace === 'analytics') return 'segment';
193
+ if (structType === 'Capture' && namespace === 'posthog') return 'posthog';
194
+ if (structType === 'Event' && namespace === 'amplitude') return 'amplitude';
195
+
196
+ // Fallback for struct types without namespace check (backward compatibility)
197
+ if (structType === 'Track') return 'segment';
198
+ if (structType === 'Capture') return 'posthog';
199
+ }
200
+ }
201
+ return null;
202
+ }
203
+
204
+ // For function calls, check if func property exists
205
+ if (!callNode.func) return null;
206
+
207
+ // Check for method calls (e.g., client.Track, mp.Track)
208
+ if (callNode.func.tag === 'access') {
209
+ const objName = callNode.func.struct?.tag === 'ident' ? callNode.func.struct.value : null;
210
+ const methodName = callNode.func.member;
211
+
212
+ if (!objName || !methodName) return null;
213
+
214
+ // Check various analytics providers
215
+ switch (true) {
216
+ // Mixpanel: mp.Track(ctx, []*mixpanel.Event{...})
217
+ case objName === 'mp' && methodName === 'Track':
218
+ return 'mixpanel';
219
+
220
+ // Amplitude: client.Track(amplitude.Event{...})
221
+ case objName === 'client' && methodName === 'Track':
222
+ return 'amplitude';
223
+
224
+ // Snowplow: tracker.TrackStructEvent(...)
225
+ case objName === 'tracker' && methodName === 'TrackStructEvent':
226
+ return 'snowplow';
227
+ }
228
+ }
229
+
230
+ // Check for custom function calls
231
+ if (customFunction && callNode.func.tag === 'ident' && callNode.func.value === customFunction) {
232
+ return 'custom';
233
+ }
234
+
235
+ return null;
236
+ }
237
+
238
+ function extractEventName(callNode, source) {
239
+ if (!callNode.args || callNode.args.length === 0) {
240
+ // For struct literals, we need to check fields instead of args
241
+ if (!callNode.fields || callNode.fields.length === 0) {
242
+ return null;
243
+ }
244
+ }
245
+
246
+ switch (source) {
247
+ case 'mixpanel':
248
+ // mp.Track(ctx, []*mixpanel.Event{mp.NewEvent("event_name", "", props)})
249
+ // Need to find the NewEvent call within the array
250
+ if (callNode.args && callNode.args.length > 1) {
251
+ const arrayArg = callNode.args[1];
252
+ if (arrayArg.tag === 'expr' && arrayArg.body) {
253
+ const arrayLit = arrayArg.body.find(item => item.tag === 'arraylit');
254
+ if (arrayLit && arrayLit.items && arrayLit.items.length > 0) {
255
+ // Each item is an array of tokens that needs to be parsed
256
+ const firstItem = arrayLit.items[0];
257
+ if (Array.isArray(firstItem)) {
258
+ // Look for pattern: mp.NewEvent("event_name", ...)
259
+ for (let i = 0; i < firstItem.length - 4; i++) {
260
+ if (firstItem[i].tag === 'ident' && firstItem[i].value === 'mp' &&
261
+ firstItem[i+1].tag === 'sigil' && firstItem[i+1].value === '.' &&
262
+ firstItem[i+2].tag === 'ident' && firstItem[i+2].value === 'NewEvent' &&
263
+ firstItem[i+3].tag === 'sigil' && firstItem[i+3].value === '(') {
264
+ // Found mp.NewEvent( - next token should be the event name
265
+ if (firstItem[i+4] && firstItem[i+4].tag === 'string') {
266
+ return firstItem[i+4].value.slice(1, -1); // Remove quotes
267
+ }
268
+ }
269
+ }
270
+ }
271
+ }
272
+ }
273
+ }
274
+ break;
275
+
276
+ case 'segment':
277
+ case 'posthog':
278
+ // analytics.Track{Event: "event_name", ...} or posthog.Capture{Event: "event_name", ...}
279
+ if (callNode.fields) {
280
+ const eventField = findStructField(callNode, 'Event');
281
+ if (eventField) {
282
+ return extractStringValue(eventField.value);
283
+ }
284
+ }
285
+ break;
286
+
287
+ case 'amplitude':
288
+ // For struct literals: amplitude.Event{EventType: "event_name", ...}
289
+ if (callNode.tag === 'structlit' && callNode.fields) {
290
+ const eventTypeField = findStructField(callNode, 'EventType');
291
+ if (eventTypeField) {
292
+ return extractStringValue(eventTypeField.value);
293
+ }
294
+ }
295
+ // For function calls: client.Track(amplitude.Event{EventType: "event_name", ...})
296
+ else if (callNode.args && callNode.args.length > 0) {
297
+ const eventStruct = findStructLiteral(callNode.args[0]);
298
+ if (eventStruct && eventStruct.fields) {
299
+ const eventTypeField = findStructField(eventStruct, 'EventType');
300
+ if (eventTypeField) {
301
+ return extractStringValue(eventTypeField.value);
302
+ }
303
+ }
304
+ }
305
+ break;
306
+
307
+ case 'snowplow':
308
+ // tracker.TrackStructEvent(sp.StructuredEvent{Action: sphelp.NewString("event_name"), ...})
309
+ if (callNode.args && callNode.args.length > 0) {
310
+ const structEvent = findStructLiteral(callNode.args[0]);
311
+ if (structEvent && structEvent.fields) {
312
+ const actionField = findStructField(structEvent, 'Action');
313
+ if (actionField) {
314
+ // Snowplow uses sphelp.NewString("value")
315
+ return extractSnowplowValue(actionField.value);
316
+ }
317
+ }
318
+ }
319
+ break;
320
+
321
+ case 'custom':
322
+ // customFunction("event_name", props)
323
+ if (callNode.args && callNode.args.length > 0) {
324
+ return extractStringValue(callNode.args[0]);
325
+ }
326
+ break;
327
+ }
328
+
329
+ return null;
330
+ }
331
+
332
+ function extractProperties(callNode, source) {
333
+ const properties = {};
334
+
335
+ switch (source) {
336
+ case 'mixpanel':
337
+ // mp.Track(ctx, []*mixpanel.Event{mp.NewEvent("event", "", map[string]any{...})})
338
+ if (callNode.args && callNode.args.length > 1) {
339
+ const arrayArg = callNode.args[1];
340
+ if (arrayArg.tag === 'expr' && arrayArg.body) {
341
+ const arrayLit = arrayArg.body.find(item => item.tag === 'arraylit');
342
+ if (arrayLit && arrayLit.items && arrayLit.items.length > 0) {
343
+ const firstItem = arrayLit.items[0];
344
+ if (Array.isArray(firstItem)) {
345
+ // Look for pattern: mp.NewEvent("event", "", map[string]any{...})
346
+ // The third argument is the properties map
347
+ let foundNewEvent = false;
348
+ for (let i = 0; i < firstItem.length - 4; i++) {
349
+ if (firstItem[i].tag === 'ident' && firstItem[i].value === 'mp' &&
350
+ firstItem[i+1].tag === 'sigil' && firstItem[i+1].value === '.' &&
351
+ firstItem[i+2].tag === 'ident' && firstItem[i+2].value === 'NewEvent' &&
352
+ firstItem[i+3].tag === 'sigil' && firstItem[i+3].value === '(') {
353
+ // Found mp.NewEvent( - skip event name and empty string to find the map
354
+ let j = i + 4;
355
+ let commaCount = 0;
356
+
357
+ // Skip to the third argument (after 2 commas)
358
+ while (j < firstItem.length && commaCount < 2) {
359
+ if (firstItem[j].tag === 'sigil' && firstItem[j].value === ',') {
360
+ commaCount++;
361
+ }
362
+ j++;
363
+ }
364
+
365
+ // Look for map[string]any{ pattern
366
+ while (j < firstItem.length - 2) {
367
+ if (firstItem[j].tag === 'ident' && firstItem[j].value === 'map' &&
368
+ firstItem[j+1].tag === 'sigil' && firstItem[j+1].value === '[') {
369
+ // Found the start of the map, now look for the opening brace
370
+ while (j < firstItem.length) {
371
+ if (firstItem[j].tag === 'sigil' && firstItem[j].value === '{') {
372
+ // Simple property extraction from tokens
373
+ // Look for pattern: "key": "value"
374
+ for (let k = j + 1; k < firstItem.length - 2; k++) {
375
+ if (firstItem[k].tag === 'string' &&
376
+ firstItem[k+1].tag === 'sigil' && firstItem[k+1].value === ':' &&
377
+ firstItem[k+2].tag === 'string') {
378
+ const key = firstItem[k].value.slice(1, -1);
379
+ properties[key] = { type: 'string' };
380
+ }
381
+ }
382
+ foundNewEvent = true;
383
+ break;
384
+ }
385
+ j++;
386
+ }
387
+ if (foundNewEvent) break;
388
+ }
389
+ j++;
390
+ }
391
+ if (foundNewEvent) break;
392
+ }
393
+ }
394
+ }
395
+ }
396
+ }
397
+ }
398
+ break;
399
+
400
+ case 'segment':
401
+ case 'posthog':
402
+ // analytics.Track{UserId: "...", Properties: analytics.NewProperties().Set(...)} or
403
+ // posthog.Capture{DistinctId: "...", Properties: posthog.NewProperties().Set(...)}
404
+ if (callNode.fields) {
405
+ // Extract UserId/DistinctId
406
+ const idField = findStructField(callNode, source === 'segment' ? 'UserId' : 'DistinctId');
407
+ if (idField) {
408
+ properties[source === 'segment' ? 'userId' : 'distinctId'] = { type: 'string' };
409
+ }
410
+
411
+ // Extract Properties
412
+ const propsField = findStructField(callNode, 'Properties');
413
+ if (propsField && propsField.value) {
414
+ if (source === 'segment') {
415
+ extractSegmentProperties(propsField.value, properties);
416
+ } else {
417
+ extractPostHogProperties(propsField.value, properties);
418
+ }
419
+ }
420
+ }
421
+ break;
422
+
423
+ case 'amplitude':
424
+ // For struct literals: amplitude.Event{UserID: "...", EventProperties: map[string]interface{}{...}}
425
+ if (callNode.tag === 'structlit' && callNode.fields) {
426
+ // Extract UserID
427
+ const userIdField = findStructField(callNode, 'UserID');
428
+ if (userIdField) {
429
+ properties.userId = { type: 'string' };
430
+ }
431
+
432
+ // Extract EventProperties
433
+ const eventPropsField = findStructField(callNode, 'EventProperties');
434
+ if (eventPropsField) {
435
+ extractPropertiesFromExpr(eventPropsField.value, properties, source);
436
+ }
437
+
438
+ // Extract EventOptions
439
+ const eventOptionsField = findStructField(callNode, 'EventOptions');
440
+ if (eventOptionsField && eventOptionsField.value) {
441
+ // Navigate through the expression body to find the structlit
442
+ const exprBody = eventOptionsField.value.body;
443
+ if (exprBody && exprBody.length >= 3) {
444
+ const structlit = exprBody[2];
445
+ if (structlit && structlit.tag === 'structlit' && structlit.fields) {
446
+ // Process each field in EventOptions
447
+ for (const field of structlit.fields) {
448
+ if (field.value && field.value.tag === 'expr' && field.value.body) {
449
+ const body = field.value.body;
450
+ if (body.length >= 3 &&
451
+ body[0].tag === 'ident' &&
452
+ body[1].tag === 'op' &&
453
+ body[1].value === ':') {
454
+ const fieldName = body[0].value;
455
+ const value = body[2];
456
+ if (value.tag === 'number') {
457
+ properties[fieldName] = { type: 'number' };
458
+ } else {
459
+ properties[fieldName] = getPropertyInfo(value);
460
+ }
461
+ }
462
+ }
463
+ }
464
+ }
465
+ }
466
+ }
467
+ }
468
+ // For function calls: client.Track(amplitude.Event{...})
469
+ else if (callNode.args && callNode.args.length > 0) {
470
+ const eventStruct = findStructLiteral(callNode.args[0]);
471
+ if (eventStruct && eventStruct.fields) {
472
+ // Extract UserID
473
+ const userIdField = findStructField(eventStruct, 'UserID');
474
+ if (userIdField) {
475
+ properties.userId = { type: 'string' };
476
+ }
477
+
478
+ // Extract EventProperties
479
+ const eventPropsField = findStructField(eventStruct, 'EventProperties');
480
+ if (eventPropsField) {
481
+ extractPropertiesFromExpr(eventPropsField.value, properties, source);
482
+ }
483
+
484
+ // Extract EventOptions
485
+ const eventOptionsField = findStructField(eventStruct, 'EventOptions');
486
+ if (eventOptionsField && eventOptionsField.value) {
487
+ // Navigate through the expression body to find the structlit
488
+ const exprBody = eventOptionsField.value.body;
489
+ if (exprBody && exprBody.length >= 3) {
490
+ const structlit = exprBody[2];
491
+ if (structlit && structlit.tag === 'structlit' && structlit.fields) {
492
+ // Process each field in EventOptions
493
+ for (const field of structlit.fields) {
494
+ if (field.value && field.value.tag === 'expr' && field.value.body) {
495
+ const body = field.value.body;
496
+ if (body.length >= 3 &&
497
+ body[0].tag === 'ident' &&
498
+ body[1].tag === 'op' &&
499
+ body[1].value === ':') {
500
+ const fieldName = body[0].value;
501
+ const value = body[2];
502
+ if (value.tag === 'number') {
503
+ properties[fieldName] = { type: 'number' };
504
+ } else {
505
+ properties[fieldName] = getPropertyInfo(value);
506
+ }
507
+ }
508
+ }
509
+ }
510
+ }
511
+ }
512
+ }
513
+ }
514
+ }
515
+ break;
516
+
517
+ case 'snowplow':
518
+ // tracker.TrackStructEvent(sp.StructuredEvent{Category: sphelp.NewString("..."), ...})
519
+ if (callNode.args && callNode.args.length > 0) {
520
+ const structEvent = findStructLiteral(callNode.args[0]);
521
+ if (structEvent && structEvent.fields) {
522
+ // Extract all fields except Action (which is the event name)
523
+ for (const field of structEvent.fields) {
524
+ const fieldName = extractFieldName(field);
525
+ if (fieldName && fieldName !== 'Action') {
526
+ const value = extractSnowplowValue(field.value);
527
+ if (value !== null) {
528
+ properties[fieldName] = { type: typeof value === 'number' ? 'number' : 'string' };
529
+ }
530
+ }
531
+ }
532
+ }
533
+ }
534
+ break;
535
+
536
+ case 'custom':
537
+ // customFunction("event", map[string]interface{}{...})
538
+ if (callNode.args && callNode.args.length > 1) {
539
+ extractPropertiesFromExpr(callNode.args[1], properties, source);
540
+ }
541
+ break;
542
+ }
543
+
544
+ return properties;
545
+ }
546
+
547
+ // Helper function to find a struct literal in an expression
548
+ function findStructLiteral(expr) {
549
+ if (!expr) return null;
550
+
551
+ if (expr.tag === 'structlit') {
552
+ return expr;
553
+ }
554
+
555
+ if (expr.tag === 'expr' && expr.body) {
556
+ for (const item of expr.body) {
557
+ if (item.tag === 'structlit') {
558
+ return item;
559
+ }
560
+ }
561
+ }
562
+
563
+ return null;
564
+ }
565
+
566
+ // Helper function to find a field in a struct by name
567
+ function findStructField(structlit, fieldName) {
568
+ if (!structlit.fields) return null;
569
+
570
+ for (const field of structlit.fields) {
571
+ const name = extractFieldName(field);
572
+ if (name === fieldName) {
573
+ return field;
574
+ }
575
+ }
576
+
577
+ return null;
578
+ }
579
+
580
+ // Helper function to extract field name from a struct field
581
+ function extractFieldName(field) {
582
+ if (field.name) {
583
+ return field.name;
584
+ }
585
+
586
+ if (field.value && field.value.tag === 'expr' && field.value.body) {
587
+ // Look for pattern: fieldName: value
588
+ const body = field.value.body;
589
+ if (body.length >= 3 &&
590
+ body[0].tag === 'ident' &&
591
+ body[1].tag === 'op' &&
592
+ body[1].value === ':') {
593
+ return body[0].value;
594
+ }
595
+ }
596
+
597
+ return null;
598
+ }
599
+
600
+ // Helper function to extract Segment/PostHog properties from NewProperties().Set() chain
601
+ function extractSegmentProperties(expr, properties) {
602
+ if (!expr) return;
603
+
604
+ // Look for method calls in the expression
605
+ if (expr.tag === 'expr' && expr.body) {
606
+ // Find the NewProperties() call in the chain
607
+ const newPropsCall = expr.body.find(item =>
608
+ item.tag === 'access' &&
609
+ item.struct &&
610
+ item.struct.tag === 'call' &&
611
+ item.struct.func &&
612
+ item.struct.func.tag === 'access' &&
613
+ item.struct.func.member === 'NewProperties'
614
+ );
615
+
616
+ if (newPropsCall) {
617
+ // Process all items in the body to find Set() calls
618
+ for (const item of expr.body) {
619
+ // Handle both direct Set() calls and Set() calls in access nodes
620
+ if (item.tag === 'call' && item.func) {
621
+ const funcName = item.func.tag === 'ident' ? item.func.value :
622
+ (item.func.tag === 'access' ? item.func.member : null);
623
+
624
+ if (funcName === 'Set' && item.args && item.args.length >= 2) {
625
+ const key = extractStringValue(item.args[0]);
626
+ if (key) {
627
+ const value = item.args[1];
628
+ // Handle different value types
629
+ if (value.tag === 'expr' && value.body) {
630
+ const firstItem = value.body[0];
631
+ if (firstItem.tag === 'string') {
632
+ properties[key] = { type: 'string' };
633
+ } else if (firstItem.tag === 'ident') {
634
+ if (firstItem.value === 'true' || firstItem.value === 'false') {
635
+ properties[key] = { type: 'boolean' };
636
+ } else if (firstItem.value === 'nil') {
637
+ properties[key] = { type: 'null' };
638
+ } else {
639
+ properties[key] = { type: 'any' };
640
+ }
641
+ } else if (firstItem.tag === 'number') {
642
+ properties[key] = { type: 'number' };
643
+ }
644
+ }
645
+ }
646
+ }
647
+ } else if (item.tag === 'access' && item.struct && item.struct.tag === 'call') {
648
+ // Handle chained Set() calls
649
+ const call = item.struct;
650
+ if (call.func && call.func.tag === 'ident' && call.func.value === 'Set' && call.args && call.args.length >= 2) {
651
+ const key = extractStringValue(call.args[0]);
652
+ if (key) {
653
+ const value = call.args[1];
654
+ // Handle different value types
655
+ if (value.tag === 'expr' && value.body) {
656
+ const firstItem = value.body[0];
657
+ if (firstItem.tag === 'string') {
658
+ properties[key] = { type: 'string' };
659
+ } else if (firstItem.tag === 'ident') {
660
+ if (firstItem.value === 'true' || firstItem.value === 'false') {
661
+ properties[key] = { type: 'boolean' };
662
+ } else if (firstItem.value === 'nil') {
663
+ properties[key] = { type: 'null' };
664
+ } else {
665
+ properties[key] = { type: 'any' };
666
+ }
667
+ } else if (firstItem.tag === 'number') {
668
+ properties[key] = { type: 'number' };
669
+ }
670
+ }
671
+ }
672
+ }
673
+ }
674
+ }
675
+ }
676
+ }
677
+ }
678
+
679
+ // Alias for PostHog since it uses the same pattern
680
+ const extractPostHogProperties = extractSegmentProperties;
681
+
682
+ // Helper function to extract Snowplow values from sphelp.NewString/NewFloat64
683
+ function extractSnowplowValue(expr) {
684
+ if (!expr) return null;
685
+
686
+ // Direct value
687
+ if (expr.tag === 'string') {
688
+ return expr.value.slice(1, -1);
689
+ }
690
+ if (expr.tag === 'number') {
691
+ return parseFloat(expr.value);
692
+ }
693
+
694
+ // Look for sphelp.NewString("value") or sphelp.NewFloat64(value)
695
+ if (expr.tag === 'expr' && expr.body) {
696
+ for (const item of expr.body) {
697
+ if (item.tag === 'call' && item.func && item.func.tag === 'access') {
698
+ if (item.func.member === 'NewString' && item.args && item.args.length > 0) {
699
+ return extractStringValue(item.args[0]);
700
+ }
701
+ if (item.func.member === 'NewFloat64' && item.args && item.args.length > 0) {
702
+ const numExpr = item.args[0];
703
+ if (numExpr.tag === 'number') {
704
+ return parseFloat(numExpr.value);
705
+ }
706
+ if (numExpr.tag === 'expr' && numExpr.body && numExpr.body[0] && numExpr.body[0].tag === 'number') {
707
+ return parseFloat(numExpr.body[0].value);
708
+ }
709
+ }
710
+ }
711
+ }
712
+ }
713
+
714
+ return null;
715
+ }
716
+
717
+ function extractStringValue(node) {
718
+ if (!node) return null;
719
+
720
+ // Handle direct string literals
721
+ if (node.tag === 'string') {
722
+ // Remove quotes from the value
723
+ return node.value.slice(1, -1);
724
+ }
725
+
726
+ // Handle expressions that might contain a string
727
+ if (node.tag === 'expr' && node.body && node.body.length > 0) {
728
+ // Look for string literals in the expression body
729
+ for (const item of node.body) {
730
+ if (item.tag === 'string') {
731
+ return item.value.slice(1, -1);
732
+ }
733
+ }
734
+ }
735
+
736
+ return null;
737
+ }
738
+
739
+ function extractPropertiesFromExpr(expr, properties, source) {
740
+ // Handle struct literals (e.g., Type{field: value})
741
+ if (expr.tag === 'structlit' && expr.fields) {
742
+ for (const field of expr.fields) {
743
+ if (field.name) {
744
+ const propInfo = getPropertyInfo(field.value);
745
+ properties[field.name] = propInfo;
746
+ } else if (field.value && field.value.tag === 'expr' && field.value.body) {
747
+ // Handle map literal fields that don't have explicit names
748
+ // Format: "key": value
749
+ const keyNode = field.value.body[0];
750
+ const colonNode = field.value.body[1];
751
+ const valueNode = field.value.body[2];
752
+
753
+ if (keyNode && keyNode.tag === 'string' && colonNode && colonNode.value === ':') {
754
+ const key = keyNode.value.slice(1, -1); // Remove quotes
755
+
756
+ // For nested maps, the value might include the map type declaration AND the structlit
757
+ if (valueNode && valueNode.tag === 'index' && valueNode.container && valueNode.container.value === 'map') {
758
+ // Look for the structlit that follows in the body
759
+ const remainingNodes = field.value.body.slice(3); // Skip key, :, and map declaration
760
+ const structlit = remainingNodes.find(node => node.tag === 'structlit');
761
+ if (structlit) {
762
+ properties[key] = getPropertyInfo(structlit);
763
+ } else {
764
+ properties[key] = { type: 'object', properties: {} };
765
+ }
766
+ } else if (valueNode) {
767
+ properties[key] = getPropertyInfo(valueNode);
768
+ }
769
+ }
770
+ }
771
+ }
772
+ }
773
+
774
+ // Handle expressions that might contain a composite literal
775
+ if (expr.tag === 'expr' && expr.body) {
776
+ for (const item of expr.body) {
777
+ if (item.tag === 'structlit') {
778
+ extractPropertiesFromExpr(item, properties, source);
779
+ } else if (item.tag === 'index' && item.container && item.container.value === 'map') {
780
+ // This is a map[string]interface{} type declaration
781
+ // Look for the following structlit
782
+ continue;
783
+ }
784
+ }
785
+ }
786
+ }
787
+
788
+ function getPropertyInfo(value) {
789
+ if (!value) return { type: 'any' };
790
+
791
+ // Handle direct values
792
+ if (value.tag === 'string') {
793
+ return { type: 'string' };
794
+ }
795
+
796
+ if (value.tag === 'number') {
797
+ return { type: 'number' };
798
+ }
799
+
800
+ if (value.tag === 'ident') {
801
+ // Check for boolean constants
802
+ if (value.value === 'true' || value.value === 'false') {
803
+ return { type: 'boolean' };
804
+ }
805
+ if (value.value === 'nil') {
806
+ return { type: 'null' };
807
+ }
808
+ // Otherwise it's a variable reference
809
+ return { type: 'any' };
810
+ }
811
+
812
+ // Handle index nodes (map[string]interface{})
813
+ if (value.tag === 'index' && value.container && value.container.value === 'map') {
814
+ // This indicates the start of a map literal, look for following structlit
815
+ return { type: 'object', properties: {} };
816
+ }
817
+
818
+ // Handle expressions
819
+ if (value.tag === 'expr' && value.body && value.body.length > 0) {
820
+ const firstItem = value.body[0];
821
+
822
+ // Check for literals
823
+ if (firstItem.tag === 'string') return { type: 'string' };
824
+ if (firstItem.tag === 'number') return { type: 'number' };
825
+ if (firstItem.tag === 'ident') {
826
+ if (firstItem.value === 'true' || firstItem.value === 'false') return { type: 'boolean' };
827
+ if (firstItem.value === 'nil') return { type: 'null' };
828
+ }
829
+
830
+ // Check for array literals
831
+ if (firstItem.tag === 'arraylit') {
832
+ return {
833
+ type: 'array',
834
+ items: { type: 'any' }
835
+ };
836
+ }
837
+
838
+ // Check for map declarations followed by struct literals
839
+ if (firstItem.tag === 'index' && firstItem.container && firstItem.container.value === 'map') {
840
+ // Look for the structlit that follows
841
+ const structlit = value.body.find(item => item.tag === 'structlit');
842
+ if (structlit && structlit.fields) {
843
+ const nestedProps = {};
844
+ // Inline the property extraction for nested objects
845
+ for (const field of structlit.fields) {
846
+ if (field.name) {
847
+ nestedProps[field.name] = getPropertyInfo(field.value);
848
+ } else if (field.value && field.value.tag === 'expr' && field.value.body) {
849
+ // Handle map literal fields
850
+ const keyNode = field.value.body[0];
851
+ const colonNode = field.value.body[1];
852
+ const valueNode = field.value.body[2];
853
+
854
+ if (keyNode && keyNode.tag === 'string' && colonNode && colonNode.value === ':') {
855
+ const key = keyNode.value.slice(1, -1);
856
+
857
+ // For nested maps, handle map declaration followed by structlit
858
+ if (valueNode && valueNode.tag === 'index' && valueNode.container && valueNode.container.value === 'map') {
859
+ const remainingNodes = field.value.body.slice(3);
860
+ const structlit = remainingNodes.find(node => node.tag === 'structlit');
861
+ if (structlit) {
862
+ nestedProps[key] = getPropertyInfo(structlit);
863
+ } else {
864
+ nestedProps[key] = { type: 'object', properties: {} };
865
+ }
866
+ } else if (valueNode) {
867
+ nestedProps[key] = getPropertyInfo(valueNode);
868
+ }
869
+ }
870
+ }
871
+ }
872
+ return {
873
+ type: 'object',
874
+ properties: nestedProps
875
+ };
876
+ }
877
+ }
878
+
879
+ // Check for struct/map literals
880
+ if (firstItem.tag === 'structlit') {
881
+ const nestedProps = {};
882
+ if (firstItem.fields) {
883
+ for (const field of firstItem.fields) {
884
+ if (field.name) {
885
+ nestedProps[field.name] = getPropertyInfo(field.value);
886
+ } else if (field.value && field.value.tag === 'expr' && field.value.body) {
887
+ // Handle map literal fields
888
+ const keyNode = field.value.body[0];
889
+ const colonNode = field.value.body[1];
890
+ const valueNode = field.value.body[2];
891
+
892
+ if (keyNode && keyNode.tag === 'string' && colonNode && colonNode.value === ':') {
893
+ const key = keyNode.value.slice(1, -1);
894
+
895
+ // For nested maps, handle map declaration followed by structlit
896
+ if (valueNode && valueNode.tag === 'index' && valueNode.container && valueNode.container.value === 'map') {
897
+ const remainingNodes = field.value.body.slice(3);
898
+ const structlit = remainingNodes.find(node => node.tag === 'structlit');
899
+ if (structlit) {
900
+ nestedProps[key] = getPropertyInfo(structlit);
901
+ } else {
902
+ nestedProps[key] = { type: 'object', properties: {} };
903
+ }
904
+ } else if (valueNode) {
905
+ nestedProps[key] = getPropertyInfo(valueNode);
906
+ }
907
+ }
908
+ }
909
+ }
910
+ }
911
+ return {
912
+ type: 'object',
913
+ properties: nestedProps
914
+ };
915
+ }
916
+ }
917
+
918
+ // Handle array literals
919
+ if (value.tag === 'arraylit') {
920
+ return {
921
+ type: 'array',
922
+ items: { type: 'any' }
923
+ };
924
+ }
925
+
926
+ // Handle struct literals (nested objects)
927
+ if (value.tag === 'structlit') {
928
+ const nestedProps = {};
929
+ if (value.fields) {
930
+ for (const field of value.fields) {
931
+ if (field.name) {
932
+ nestedProps[field.name] = getPropertyInfo(field.value);
933
+ } else if (field.value && field.value.tag === 'expr' && field.value.body) {
934
+ // Handle map literal fields
935
+ const keyNode = field.value.body[0];
936
+ const colonNode = field.value.body[1];
937
+ const valueNode = field.value.body[2];
938
+
939
+ if (keyNode && keyNode.tag === 'string' && colonNode && colonNode.value === ':') {
940
+ const key = keyNode.value.slice(1, -1);
941
+
942
+ // For nested maps, handle map declaration followed by structlit
943
+ if (valueNode && valueNode.tag === 'index' && valueNode.container && valueNode.container.value === 'map') {
944
+ const remainingNodes = field.value.body.slice(3);
945
+ const structlit = remainingNodes.find(node => node.tag === 'structlit');
946
+ if (structlit) {
947
+ nestedProps[key] = getPropertyInfo(structlit);
948
+ } else {
949
+ nestedProps[key] = { type: 'object', properties: {} };
950
+ }
951
+ } else if (valueNode) {
952
+ nestedProps[key] = getPropertyInfo(valueNode);
953
+ }
954
+ }
955
+ }
956
+ }
957
+ }
958
+ return {
959
+ type: 'object',
960
+ properties: nestedProps
961
+ };
962
+ }
963
+
964
+ // Default to any type
965
+ return { type: 'any' };
966
+ }
967
+
968
+ module.exports = { analyzeGoFile };