@flisk/analyze-tracking 0.7.1 → 0.7.3

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.
Files changed (71) hide show
  1. package/README.md +35 -61
  2. package/bin/cli.js +1 -1
  3. package/package.json +18 -3
  4. package/src/analyze/go/astTraversal.js +121 -0
  5. package/src/analyze/go/constants.js +20 -0
  6. package/src/analyze/go/eventDeduplicator.js +47 -0
  7. package/src/analyze/go/eventExtractor.js +156 -0
  8. package/src/analyze/go/goAstParser/constants.js +39 -0
  9. package/src/analyze/go/goAstParser/expressionParser.js +281 -0
  10. package/src/analyze/go/goAstParser/index.js +52 -0
  11. package/src/analyze/go/goAstParser/statementParser.js +387 -0
  12. package/src/analyze/go/goAstParser/tokenizer.js +196 -0
  13. package/src/analyze/go/goAstParser/typeParser.js +202 -0
  14. package/src/analyze/go/goAstParser/utils.js +99 -0
  15. package/src/analyze/go/index.js +55 -0
  16. package/src/analyze/go/propertyExtractor.js +670 -0
  17. package/src/analyze/go/trackingDetector.js +71 -0
  18. package/src/analyze/go/trackingExtractor.js +54 -0
  19. package/src/analyze/go/typeContext.js +88 -0
  20. package/src/analyze/go/utils.js +215 -0
  21. package/src/analyze/index.js +11 -7
  22. package/src/analyze/javascript/constants.js +115 -0
  23. package/src/analyze/javascript/detectors/analytics-source.js +119 -0
  24. package/src/analyze/javascript/detectors/index.js +10 -0
  25. package/src/analyze/javascript/extractors/event-extractor.js +179 -0
  26. package/src/analyze/javascript/extractors/index.js +13 -0
  27. package/src/analyze/javascript/extractors/property-extractor.js +172 -0
  28. package/src/analyze/javascript/index.js +38 -0
  29. package/src/analyze/javascript/parser.js +126 -0
  30. package/src/analyze/javascript/utils/function-finder.js +123 -0
  31. package/src/analyze/python/index.js +111 -0
  32. package/src/analyze/python/pythonTrackingAnalyzer.py +814 -0
  33. package/src/analyze/ruby/detectors.js +46 -0
  34. package/src/analyze/ruby/extractors.js +258 -0
  35. package/src/analyze/ruby/index.js +51 -0
  36. package/src/analyze/ruby/traversal.js +123 -0
  37. package/src/analyze/ruby/types.js +30 -0
  38. package/src/analyze/ruby/visitor.js +66 -0
  39. package/src/analyze/typescript/constants.js +109 -0
  40. package/src/analyze/typescript/detectors/analytics-source.js +120 -0
  41. package/src/analyze/typescript/detectors/index.js +10 -0
  42. package/src/analyze/typescript/extractors/event-extractor.js +269 -0
  43. package/src/analyze/typescript/extractors/index.js +14 -0
  44. package/src/analyze/typescript/extractors/property-extractor.js +395 -0
  45. package/src/analyze/typescript/index.js +48 -0
  46. package/src/analyze/typescript/parser.js +131 -0
  47. package/src/analyze/typescript/utils/function-finder.js +114 -0
  48. package/src/analyze/typescript/utils/type-resolver.js +193 -0
  49. package/src/generateDescriptions/index.js +81 -0
  50. package/src/generateDescriptions/llmUtils.js +33 -0
  51. package/src/generateDescriptions/promptUtils.js +62 -0
  52. package/src/generateDescriptions/schemaUtils.js +61 -0
  53. package/src/index.js +7 -2
  54. package/src/{fileProcessor.js → utils/fileProcessor.js} +5 -0
  55. package/src/{repoDetails.js → utils/repoDetails.js} +5 -0
  56. package/src/{yamlGenerator.js → utils/yamlGenerator.js} +5 -0
  57. package/.github/workflows/npm-publish.yml +0 -33
  58. package/.github/workflows/pr-check.yml +0 -17
  59. package/jest.config.js +0 -7
  60. package/src/analyze/analyzeGoFile.js +0 -1164
  61. package/src/analyze/analyzeJsFile.js +0 -72
  62. package/src/analyze/analyzePythonFile.js +0 -41
  63. package/src/analyze/analyzeRubyFile.js +0 -409
  64. package/src/analyze/analyzeTsFile.js +0 -69
  65. package/src/analyze/go2json.js +0 -1069
  66. package/src/analyze/helpers.js +0 -217
  67. package/src/analyze/pythonTrackingAnalyzer.py +0 -439
  68. package/src/generateDescriptions.js +0 -196
  69. package/tests/detectSource.test.js +0 -20
  70. package/tests/extractProperties.test.js +0 -109
  71. package/tests/findWrappingFunction.test.js +0 -30
@@ -1,1164 +0,0 @@
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
- // First pass: build type information for functions and variables
14
- const typeContext = buildTypeContext(ast);
15
-
16
- // Extract tracking events from the AST
17
- const events = [];
18
- let currentFunction = 'global';
19
-
20
- // Walk through the AST
21
- for (const node of ast) {
22
- if (node.tag === 'func') {
23
- currentFunction = node.name;
24
- // Process the function body
25
- if (node.body) {
26
- extractEventsFromBody(node.body, events, filePath, currentFunction, customFunction, typeContext, currentFunction);
27
- }
28
- }
29
- }
30
-
31
- // Deduplicate events based on eventName, source, and function
32
- const uniqueEvents = [];
33
- const seen = new Set();
34
-
35
- for (const event of events) {
36
- // For Amplitude, we want to keep the line number from the struct literal
37
- // For other sources, we can use any line number since they don't have this issue
38
- const key = `${event.eventName}:${event.source}:${event.functionName}`;
39
- if (!seen.has(key)) {
40
- seen.add(key);
41
- uniqueEvents.push(event);
42
- } else {
43
- // If we've seen this event before and it's Amplitude, check if this is the struct literal version
44
- const existingEvent = uniqueEvents.find(e =>
45
- e.eventName === event.eventName &&
46
- e.source === event.source &&
47
- e.functionName === event.functionName
48
- );
49
-
50
- // If this is Amplitude and the existing event is from the function call (higher line number),
51
- // replace it with this one (from the struct literal)
52
- if (event.source === 'amplitude' && existingEvent && existingEvent.line > event.line) {
53
- const index = uniqueEvents.indexOf(existingEvent);
54
- uniqueEvents[index] = event;
55
- }
56
- }
57
- }
58
-
59
- return uniqueEvents;
60
- } catch (error) {
61
- console.error(`Error analyzing Go file ${filePath}:`, error.message);
62
- return [];
63
- }
64
- }
65
-
66
- // Build a context of type information from the AST
67
- function buildTypeContext(ast) {
68
- const context = {
69
- functions: {},
70
- globals: {}
71
- };
72
-
73
- for (const node of ast) {
74
- if (node.tag === 'func') {
75
- // Store function parameter types
76
- context.functions[node.name] = {
77
- params: {},
78
- locals: {}
79
- };
80
-
81
- if (node.args) {
82
- for (const arg of node.args) {
83
- if (arg.name && arg.type) {
84
- context.functions[node.name].params[arg.name] = { type: arg.type };
85
- }
86
- }
87
- }
88
-
89
- // Scan function body for local variable declarations
90
- if (node.body) {
91
- scanForDeclarations(node.body, context.functions[node.name].locals);
92
- }
93
- } else if (node.tag === 'declare') {
94
- // Global variable declarations
95
- if (node.names && node.names.length > 0 && node.type) {
96
- for (const name of node.names) {
97
- context.globals[name] = { type: node.type, value: node.value };
98
- }
99
- }
100
- }
101
- }
102
-
103
- return context;
104
- }
105
-
106
- // Scan statements for variable declarations
107
- function scanForDeclarations(body, locals) {
108
- for (const stmt of body) {
109
- if (stmt.tag === 'declare') {
110
- if (stmt.names && stmt.type) {
111
- for (const name of stmt.names) {
112
- locals[name] = { type: stmt.type, value: stmt.value };
113
- }
114
- }
115
- } else if (stmt.tag === 'if' && stmt.body) {
116
- scanForDeclarations(stmt.body, locals);
117
- } else if (stmt.tag === 'elseif' && stmt.body) {
118
- scanForDeclarations(stmt.body, locals);
119
- } else if (stmt.tag === 'else' && stmt.body) {
120
- scanForDeclarations(stmt.body, locals);
121
- } else if (stmt.tag === 'for' && stmt.body) {
122
- scanForDeclarations(stmt.body, locals);
123
- } else if (stmt.tag === 'foreach' && stmt.body) {
124
- scanForDeclarations(stmt.body, locals);
125
- } else if (stmt.tag === 'switch' && stmt.cases) {
126
- for (const caseNode of stmt.cases) {
127
- if (caseNode.body) {
128
- scanForDeclarations(caseNode.body, locals);
129
- }
130
- }
131
- }
132
- }
133
- }
134
-
135
- function extractEventsFromBody(body, events, filePath, functionName, customFunction, typeContext, currentFunction) {
136
- for (const stmt of body) {
137
- if (stmt.tag === 'exec' && stmt.expr) {
138
- processExpression(stmt.expr, events, filePath, functionName, customFunction, typeContext, currentFunction);
139
- } else if (stmt.tag === 'declare' && stmt.value) {
140
- // Handle variable declarations with tracking calls
141
- processExpression(stmt.value, events, filePath, functionName, customFunction, typeContext, currentFunction);
142
- } else if (stmt.tag === 'assign' && stmt.rhs) {
143
- // Handle assignments with tracking calls
144
- processExpression(stmt.rhs, events, filePath, functionName, customFunction, typeContext, currentFunction);
145
- } else if (stmt.tag === 'if' && stmt.body) {
146
- extractEventsFromBody(stmt.body, events, filePath, functionName, customFunction, typeContext, currentFunction);
147
- } else if (stmt.tag === 'elseif' && stmt.body) {
148
- extractEventsFromBody(stmt.body, events, filePath, functionName, customFunction, typeContext, currentFunction);
149
- } else if (stmt.tag === 'else' && stmt.body) {
150
- extractEventsFromBody(stmt.body, events, filePath, functionName, customFunction, typeContext, currentFunction);
151
- } else if (stmt.tag === 'for' && stmt.body) {
152
- extractEventsFromBody(stmt.body, events, filePath, functionName, customFunction, typeContext, currentFunction);
153
- } else if (stmt.tag === 'foreach' && stmt.body) {
154
- extractEventsFromBody(stmt.body, events, filePath, functionName, customFunction, typeContext, currentFunction);
155
- } else if (stmt.tag === 'switch' && stmt.cases) {
156
- for (const caseNode of stmt.cases) {
157
- if (caseNode.body) {
158
- extractEventsFromBody(caseNode.body, events, filePath, functionName, customFunction, typeContext, currentFunction);
159
- }
160
- }
161
- }
162
- }
163
- }
164
-
165
- function processExpression(expr, events, filePath, functionName, customFunction, typeContext, currentFunction, depth = 0) {
166
- if (!expr || depth > 20) return; // Prevent infinite recursion with depth limit
167
-
168
- // Handle array of expressions
169
- if (Array.isArray(expr)) {
170
- for (const item of expr) {
171
- processExpression(item, events, filePath, functionName, customFunction, typeContext, currentFunction, depth + 1);
172
- }
173
- return;
174
- }
175
-
176
- // Handle single expression with body
177
- if (expr.body) {
178
- for (const item of expr.body) {
179
- processExpression(item, events, filePath, functionName, customFunction, typeContext, currentFunction, depth + 1);
180
- }
181
- return;
182
- }
183
-
184
- // Handle specific node types
185
- if (expr.tag === 'call') {
186
- const trackingCall = extractTrackingCall(expr, filePath, functionName, customFunction, typeContext, currentFunction);
187
- if (trackingCall) {
188
- events.push(trackingCall);
189
- }
190
-
191
- // Also process call arguments
192
- if (expr.args) {
193
- processExpression(expr.args, events, filePath, functionName, customFunction, typeContext, currentFunction, depth + 1);
194
- }
195
- } else if (expr.tag === 'structlit') {
196
- // Check if this struct literal is a tracking event
197
- const trackingCall = extractTrackingCall(expr, filePath, functionName, customFunction, typeContext, currentFunction);
198
- if (trackingCall) {
199
- events.push(trackingCall);
200
- }
201
-
202
- // Process fields (but don't recurse into field values for tracking structs)
203
- if (!trackingCall && expr.fields) {
204
- for (const field of expr.fields) {
205
- if (field.value) {
206
- processExpression(field.value, events, filePath, functionName, customFunction, typeContext, currentFunction, depth + 1);
207
- }
208
- }
209
- }
210
- }
211
-
212
- // Process other common properties that might contain expressions
213
- if (expr.value && expr.tag !== 'structlit') {
214
- processExpression(expr.value, events, filePath, functionName, customFunction, typeContext, currentFunction, depth + 1);
215
- }
216
- if (expr.lhs) {
217
- processExpression(expr.lhs, events, filePath, functionName, customFunction, typeContext, currentFunction, depth + 1);
218
- }
219
- if (expr.rhs) {
220
- processExpression(expr.rhs, events, filePath, functionName, customFunction, typeContext, currentFunction, depth + 1);
221
- }
222
- }
223
-
224
- function extractTrackingCall(callNode, filePath, functionName, customFunction, typeContext, currentFunction) {
225
- const source = detectSource(callNode, customFunction);
226
- if (!source) return null;
227
-
228
- const eventName = extractEventName(callNode, source);
229
- if (!eventName) return null;
230
-
231
- const properties = extractProperties(callNode, source, typeContext, currentFunction);
232
-
233
- // Get line number based on source type
234
- let line = 0;
235
- if (source === 'segment' || source === 'posthog') {
236
- // For Segment and PostHog, we need to get the line number from the struct.struct object
237
- if (callNode.tag === 'structlit' && callNode.struct && callNode.struct.struct) {
238
- line = callNode.struct.struct.line || 0;
239
- }
240
- } else {
241
- // For other sources, use the line number from the AST node
242
- line = callNode.line || 0;
243
- }
244
-
245
- return {
246
- eventName,
247
- source,
248
- properties,
249
- filePath,
250
- line,
251
- functionName
252
- };
253
- }
254
-
255
- function detectSource(callNode, customFunction) {
256
- // Check for struct literals (Segment/Rudderstack/PostHog/Amplitude)
257
- if (callNode.tag === 'structlit') {
258
- if (callNode.struct) {
259
- if (callNode.struct.tag === 'access') {
260
- const structType = callNode.struct.member;
261
- const namespace = callNode.struct.struct?.value;
262
-
263
- // Check for specific struct types with their namespaces
264
- if (structType === 'Track' && namespace === 'analytics') return 'segment';
265
- if (structType === 'Capture' && namespace === 'posthog') return 'posthog';
266
- if (structType === 'Event' && namespace === 'amplitude') return 'amplitude';
267
-
268
- // Fallback for struct types without namespace check (backward compatibility)
269
- if (structType === 'Track') return 'segment';
270
- if (structType === 'Capture') return 'posthog';
271
- }
272
- }
273
- return null;
274
- }
275
-
276
- // For function calls, check if func property exists
277
- if (!callNode.func) return null;
278
-
279
- // Check for method calls (e.g., client.Track, mp.Track)
280
- if (callNode.func.tag === 'access') {
281
- const objName = callNode.func.struct?.tag === 'ident' ? callNode.func.struct.value : null;
282
- const methodName = callNode.func.member;
283
-
284
- if (!objName || !methodName) return null;
285
-
286
- // Check various analytics providers
287
- switch (true) {
288
- // Mixpanel: mp.Track(ctx, []*mixpanel.Event{...})
289
- case objName === 'mp' && methodName === 'Track':
290
- return 'mixpanel';
291
-
292
- // Amplitude: client.Track(amplitude.Event{...})
293
- case objName === 'client' && methodName === 'Track':
294
- return 'amplitude';
295
-
296
- // Snowplow: tracker.TrackStructEvent(...)
297
- case objName === 'tracker' && methodName === 'TrackStructEvent':
298
- return 'snowplow';
299
- }
300
- }
301
-
302
- // Check for custom function calls
303
- if (customFunction && callNode.func.tag === 'ident' && callNode.func.value === customFunction) {
304
- return 'custom';
305
- }
306
-
307
- return null;
308
- }
309
-
310
- function extractEventName(callNode, source) {
311
- if (!callNode.args || callNode.args.length === 0) {
312
- // For struct literals, we need to check fields instead of args
313
- if (!callNode.fields || callNode.fields.length === 0) {
314
- return null;
315
- }
316
- }
317
-
318
- switch (source) {
319
- case 'mixpanel':
320
- // mp.Track(ctx, []*mixpanel.Event{mp.NewEvent("event_name", "", props)})
321
- // Need to find the NewEvent call within the array
322
- if (callNode.args && callNode.args.length > 1) {
323
- const arrayArg = callNode.args[1];
324
- if (arrayArg.tag === 'expr' && arrayArg.body) {
325
- const arrayLit = arrayArg.body.find(item => item.tag === 'arraylit');
326
- if (arrayLit && arrayLit.items && arrayLit.items.length > 0) {
327
- // Each item is an array of tokens that needs to be parsed
328
- const firstItem = arrayLit.items[0];
329
- if (Array.isArray(firstItem)) {
330
- // Look for pattern: mp.NewEvent("event_name", ...)
331
- for (let i = 0; i < firstItem.length - 4; i++) {
332
- if (firstItem[i].tag === 'ident' && firstItem[i].value === 'mp' &&
333
- firstItem[i+1].tag === 'sigil' && firstItem[i+1].value === '.' &&
334
- firstItem[i+2].tag === 'ident' && firstItem[i+2].value === 'NewEvent' &&
335
- firstItem[i+3].tag === 'sigil' && firstItem[i+3].value === '(') {
336
- // Found mp.NewEvent( - next token should be the event name
337
- if (firstItem[i+4] && firstItem[i+4].tag === 'string') {
338
- return firstItem[i+4].value.slice(1, -1); // Remove quotes
339
- }
340
- }
341
- }
342
- }
343
- }
344
- }
345
- }
346
- break;
347
-
348
- case 'segment':
349
- case 'posthog':
350
- // analytics.Track{Event: "event_name", ...} or posthog.Capture{Event: "event_name", ...}
351
- if (callNode.fields) {
352
- const eventField = findStructField(callNode, 'Event');
353
- if (eventField) {
354
- return extractStringValue(eventField.value);
355
- }
356
- }
357
- break;
358
-
359
- case 'amplitude':
360
- // For struct literals: amplitude.Event{EventType: "event_name", ...}
361
- if (callNode.tag === 'structlit' && callNode.fields) {
362
- const eventTypeField = findStructField(callNode, 'EventType');
363
- if (eventTypeField) {
364
- return extractStringValue(eventTypeField.value);
365
- }
366
- }
367
- // For function calls: client.Track(amplitude.Event{EventType: "event_name", ...})
368
- else if (callNode.args && callNode.args.length > 0) {
369
- const eventStruct = findStructLiteral(callNode.args[0]);
370
- if (eventStruct && eventStruct.fields) {
371
- const eventTypeField = findStructField(eventStruct, 'EventType');
372
- if (eventTypeField) {
373
- return extractStringValue(eventTypeField.value);
374
- }
375
- }
376
- }
377
- break;
378
-
379
- case 'snowplow':
380
- // tracker.TrackStructEvent(sp.StructuredEvent{Action: sphelp.NewString("event_name"), ...})
381
- if (callNode.args && callNode.args.length > 0) {
382
- const structEvent = findStructLiteral(callNode.args[0]);
383
- if (structEvent && structEvent.fields) {
384
- const actionField = findStructField(structEvent, 'Action');
385
- if (actionField) {
386
- // Snowplow uses sphelp.NewString("value")
387
- return extractSnowplowValue(actionField.value);
388
- }
389
- }
390
- }
391
- break;
392
-
393
- case 'custom':
394
- // customFunction("event_name", props)
395
- if (callNode.args && callNode.args.length > 0) {
396
- return extractStringValue(callNode.args[0]);
397
- }
398
- break;
399
- }
400
-
401
- return null;
402
- }
403
-
404
- function extractProperties(callNode, source, typeContext, currentFunction) {
405
- const properties = {};
406
-
407
- switch (source) {
408
- case 'mixpanel':
409
- // mp.Track(ctx, []*mixpanel.Event{mp.NewEvent("event", "distinctId", map[string]any{...})})
410
- if (callNode.args && callNode.args.length > 1) {
411
- const arrayArg = callNode.args[1];
412
- if (arrayArg.tag === 'expr' && arrayArg.body) {
413
- const arrayLit = arrayArg.body.find(item => item.tag === 'arraylit');
414
- if (arrayLit && arrayLit.items && arrayLit.items.length > 0) {
415
- const firstItem = arrayLit.items[0];
416
- if (Array.isArray(firstItem)) {
417
- // Look for pattern: mp.NewEvent("event", "distinctId", map[string]any{...})
418
- let foundNewEvent = false;
419
- for (let i = 0; i < firstItem.length - 4; i++) {
420
- if (firstItem[i].tag === 'ident' && firstItem[i].value === 'mp' &&
421
- firstItem[i+1].tag === 'sigil' && firstItem[i+1].value === '.' &&
422
- firstItem[i+2].tag === 'ident' && firstItem[i+2].value === 'NewEvent' &&
423
- firstItem[i+3].tag === 'sigil' && firstItem[i+3].value === '(') {
424
- // Found mp.NewEvent( - process arguments
425
- let j = i + 4;
426
- let commaCount = 0;
427
- let distinctIdToken = null;
428
-
429
- // Skip the first argument (event name)
430
- while (j < firstItem.length && commaCount < 1) {
431
- if (firstItem[j].tag === 'sigil' && firstItem[j].value === ',') {
432
- commaCount++;
433
- }
434
- j++;
435
- }
436
-
437
- // Extract the second argument (DistinctId)
438
- if (j < firstItem.length) {
439
- // Skip whitespace
440
- while (j < firstItem.length && firstItem[j].tag === 'newline') {
441
- j++;
442
- }
443
-
444
- if (firstItem[j]) {
445
- if (firstItem[j].tag === 'string') {
446
- // It's a string literal
447
- const distinctId = firstItem[j].value.slice(1, -1); // Remove quotes
448
- if (distinctId !== '') { // Only add if not empty string
449
- properties['DistinctId'] = { type: 'string' };
450
- }
451
- } else if (firstItem[j].tag === 'ident') {
452
- // It's a variable reference - look up its type
453
- properties['DistinctId'] = getPropertyInfo(firstItem[j], typeContext, currentFunction);
454
- }
455
- }
456
- }
457
-
458
- // Continue to find the properties map (third argument)
459
- while (j < firstItem.length && commaCount < 2) {
460
- if (firstItem[j].tag === 'sigil' && firstItem[j].value === ',') {
461
- commaCount++;
462
- }
463
- j++;
464
- }
465
-
466
- // Look for map[string]any{ pattern
467
- while (j < firstItem.length - 2) {
468
- if (firstItem[j].tag === 'ident' && firstItem[j].value === 'map' &&
469
- firstItem[j+1].tag === 'sigil' && firstItem[j+1].value === '[') {
470
- // Found the start of the map, now look for the opening brace
471
- while (j < firstItem.length) {
472
- if (firstItem[j].tag === 'sigil' && firstItem[j].value === '{') {
473
- // Simple property extraction from tokens
474
- // Look for pattern: "key": value
475
- for (let k = j + 1; k < firstItem.length - 2; k++) {
476
- if (firstItem[k].tag === 'string' &&
477
- firstItem[k+1].tag === 'sigil' && firstItem[k+1].value === ':') {
478
- const key = firstItem[k].value.slice(1, -1);
479
-
480
- // Use getPropertyInfo to determine the type
481
- const valueToken = firstItem[k+2];
482
- properties[key] = getPropertyInfo(valueToken, typeContext, currentFunction);
483
- }
484
- }
485
- foundNewEvent = true;
486
- break;
487
- }
488
- j++;
489
- }
490
- if (foundNewEvent) break;
491
- }
492
- j++;
493
- }
494
- if (foundNewEvent) break;
495
- }
496
- }
497
- }
498
- }
499
- }
500
- }
501
- break;
502
-
503
- case 'segment':
504
- case 'posthog':
505
- // analytics.Track{UserId: "...", Properties: analytics.NewProperties().Set(...)} or
506
- // posthog.Capture{DistinctId: "...", Properties: posthog.NewProperties().Set(...)}
507
- if (callNode.fields) {
508
- // Extract UserId/DistinctId
509
- const idField = findStructField(callNode, source === 'segment' ? 'UserId' : 'DistinctId');
510
- if (idField) {
511
- properties[source === 'segment' ? 'UserId' : 'DistinctId'] = { type: 'string' };
512
- }
513
-
514
- // Extract Properties
515
- const propsField = findStructField(callNode, 'Properties');
516
- if (propsField && propsField.value) {
517
- if (source === 'segment') {
518
- extractSegmentProperties(propsField.value, properties, typeContext, currentFunction);
519
- } else {
520
- extractPostHogProperties(propsField.value, properties, typeContext, currentFunction);
521
- }
522
- }
523
- }
524
- break;
525
-
526
- case 'amplitude':
527
- // For struct literals: amplitude.Event{UserID: "...", EventProperties: map[string]interface{}{...}}
528
- if (callNode.tag === 'structlit' && callNode.fields) {
529
- // Extract UserID
530
- const userIdField = findStructField(callNode, 'UserID');
531
- if (userIdField) {
532
- properties['UserID'] = { type: 'string' };
533
- }
534
-
535
- // Extract EventProperties
536
- const eventPropsField = findStructField(callNode, 'EventProperties');
537
- if (eventPropsField) {
538
- extractPropertiesFromExpr(eventPropsField.value, properties, source, typeContext, currentFunction);
539
- }
540
-
541
- // Extract EventOptions
542
- const eventOptionsField = findStructField(callNode, 'EventOptions');
543
- if (eventOptionsField && eventOptionsField.value) {
544
- // Navigate through the expression body to find the structlit
545
- const exprBody = eventOptionsField.value.body;
546
- if (exprBody && exprBody.length >= 3) {
547
- const structlit = exprBody[2];
548
- if (structlit && structlit.tag === 'structlit' && structlit.fields) {
549
- // Process each field in EventOptions
550
- for (const field of structlit.fields) {
551
- if (field.value && field.value.tag === 'expr' && field.value.body) {
552
- const body = field.value.body;
553
- if (body.length >= 3 &&
554
- body[0].tag === 'ident' &&
555
- body[1].tag === 'op' &&
556
- body[1].value === ':') {
557
- const fieldName = body[0].value;
558
- const value = body[2];
559
- if (value.tag === 'number') {
560
- properties[fieldName] = { type: 'number' };
561
- } else {
562
- properties[fieldName] = getPropertyInfo(value, typeContext, currentFunction);
563
- }
564
- }
565
- }
566
- }
567
- }
568
- }
569
- }
570
- }
571
- // For function calls: client.Track(amplitude.Event{...})
572
- else if (callNode.args && callNode.args.length > 0) {
573
- const eventStruct = findStructLiteral(callNode.args[0]);
574
- if (eventStruct && eventStruct.fields) {
575
- // Extract UserID
576
- const userIdField = findStructField(eventStruct, 'UserID');
577
- if (userIdField) {
578
- properties['UserID'] = { type: 'string' };
579
- }
580
-
581
- // Extract EventProperties
582
- const eventPropsField = findStructField(eventStruct, 'EventProperties');
583
- if (eventPropsField) {
584
- extractPropertiesFromExpr(eventPropsField.value, properties, source, typeContext, currentFunction);
585
- }
586
-
587
- // Extract EventOptions
588
- const eventOptionsField = findStructField(eventStruct, 'EventOptions');
589
- if (eventOptionsField && eventOptionsField.value) {
590
- // Navigate through the expression body to find the structlit
591
- const exprBody = eventOptionsField.value.body;
592
- if (exprBody && exprBody.length >= 3) {
593
- const structlit = exprBody[2];
594
- if (structlit && structlit.tag === 'structlit' && structlit.fields) {
595
- // Process each field in EventOptions
596
- for (const field of structlit.fields) {
597
- if (field.value && field.value.tag === 'expr' && field.value.body) {
598
- const body = field.value.body;
599
- if (body.length >= 3 &&
600
- body[0].tag === 'ident' &&
601
- body[1].tag === 'op' &&
602
- body[1].value === ':') {
603
- const fieldName = body[0].value;
604
- const value = body[2];
605
- if (value.tag === 'number') {
606
- properties[fieldName] = { type: 'number' };
607
- } else {
608
- properties[fieldName] = getPropertyInfo(value, typeContext, currentFunction);
609
- }
610
- }
611
- }
612
- }
613
- }
614
- }
615
- }
616
- }
617
- }
618
- break;
619
-
620
- case 'snowplow':
621
- // tracker.TrackStructEvent(sp.StructuredEvent{Category: sphelp.NewString("..."), ...})
622
- if (callNode.args && callNode.args.length > 0) {
623
- const structEvent = findStructLiteral(callNode.args[0]);
624
- if (structEvent && structEvent.fields) {
625
- // Extract all fields except Action (which is the event name)
626
- for (const field of structEvent.fields) {
627
- const fieldName = extractFieldName(field);
628
- if (fieldName && fieldName !== 'Action') {
629
- // Handle both direct values and sphelp.NewString/NewFloat64 calls
630
- if (field.value) {
631
- if (field.value.tag === 'expr' && field.value.body) {
632
- // Look for sphelp.NewString/NewFloat64 calls
633
- const callNode = field.value.body.find(item =>
634
- item.tag === 'call' &&
635
- item.func &&
636
- item.func.tag === 'access' &&
637
- (item.func.member === 'NewString' || item.func.member === 'NewFloat64')
638
- );
639
-
640
- if (callNode && callNode.args && callNode.args.length > 0) {
641
- const value = callNode.args[0];
642
-
643
- // Handle case where value is an expr with the actual value in body[0]
644
- let actualValue = value;
645
- if (value.tag === 'expr' && value.body && value.body.length > 0) {
646
- actualValue = value.body[0];
647
- }
648
-
649
- // Use getPropertyInfo to handle all value types including variables
650
- properties[fieldName] = getPropertyInfo(actualValue, typeContext, currentFunction);
651
- }
652
- } else {
653
- // Handle direct values using getPropertyInfo
654
- properties[fieldName] = getPropertyInfo(field.value, typeContext, currentFunction);
655
- }
656
- }
657
- }
658
- }
659
- }
660
- }
661
- break;
662
-
663
- case 'custom':
664
- // customFunction("event", map[string]interface{}{...})
665
- if (callNode.args && callNode.args.length > 1) {
666
- extractPropertiesFromExpr(callNode.args[1], properties, source, typeContext, currentFunction);
667
- }
668
- break;
669
- }
670
-
671
- return properties;
672
- }
673
-
674
- // Helper function to find a struct literal in an expression
675
- function findStructLiteral(expr) {
676
- if (!expr) return null;
677
-
678
- if (expr.tag === 'structlit') {
679
- return expr;
680
- }
681
-
682
- if (expr.tag === 'expr' && expr.body) {
683
- for (const item of expr.body) {
684
- if (item.tag === 'structlit') {
685
- return item;
686
- }
687
- }
688
- }
689
-
690
- return null;
691
- }
692
-
693
- // Helper function to find a field in a struct by name
694
- function findStructField(structlit, fieldName) {
695
- if (!structlit.fields) return null;
696
-
697
- for (const field of structlit.fields) {
698
- const name = extractFieldName(field);
699
- if (name === fieldName) {
700
- return field;
701
- }
702
- }
703
-
704
- return null;
705
- }
706
-
707
- // Helper function to extract field name from a struct field
708
- function extractFieldName(field) {
709
- if (field.name) {
710
- return field.name;
711
- }
712
-
713
- if (field.value && field.value.tag === 'expr' && field.value.body) {
714
- // Look for pattern: fieldName: value
715
- const body = field.value.body;
716
- if (body.length >= 3 &&
717
- body[0].tag === 'ident' &&
718
- body[1].tag === 'op' &&
719
- body[1].value === ':') {
720
- return body[0].value;
721
- }
722
- }
723
-
724
- return null;
725
- }
726
-
727
- // Helper function to extract Segment/PostHog properties from NewProperties().Set() chain
728
- function extractSegmentProperties(expr, properties, typeContext, currentFunction) {
729
- if (!expr) return;
730
-
731
- // Look for method calls in the expression
732
- if (expr.tag === 'expr' && expr.body) {
733
- // Find the NewProperties() call in the chain
734
- const newPropsCall = expr.body.find(item =>
735
- item.tag === 'access' &&
736
- item.struct &&
737
- item.struct.tag === 'call' &&
738
- item.struct.func &&
739
- item.struct.func.tag === 'access' &&
740
- item.struct.func.member === 'NewProperties'
741
- );
742
-
743
- if (newPropsCall) {
744
- // Process all items in the body to find Set() calls
745
- for (const item of expr.body) {
746
- // Handle both direct Set() calls and Set() calls in access nodes
747
- if (item.tag === 'call' && item.func) {
748
- const funcName = item.func.tag === 'ident' ? item.func.value :
749
- (item.func.tag === 'access' ? item.func.member : null);
750
-
751
- if (funcName === 'Set' && item.args && item.args.length >= 2) {
752
- const key = extractStringValue(item.args[0]);
753
- if (key) {
754
- const value = item.args[1];
755
- // Handle different value types
756
- if (value.tag === 'expr' && value.body) {
757
- const firstItem = value.body[0];
758
- properties[key] = getPropertyInfo(firstItem, typeContext, currentFunction);
759
- } else {
760
- properties[key] = getPropertyInfo(value, typeContext, currentFunction);
761
- }
762
- }
763
- }
764
- } else if (item.tag === 'access' && item.struct && item.struct.tag === 'call') {
765
- // Handle chained Set() calls
766
- const call = item.struct;
767
- if (call.func && call.func.tag === 'ident' && call.func.value === 'Set' && call.args && call.args.length >= 2) {
768
- const key = extractStringValue(call.args[0]);
769
- if (key) {
770
- const value = call.args[1];
771
- // Handle different value types
772
- if (value.tag === 'expr' && value.body) {
773
- const firstItem = value.body[0];
774
- properties[key] = getPropertyInfo(firstItem, typeContext, currentFunction);
775
- } else {
776
- properties[key] = getPropertyInfo(value, typeContext, currentFunction);
777
- }
778
- }
779
- }
780
- }
781
- }
782
- }
783
- }
784
- }
785
-
786
- // Alias for PostHog since it uses the same pattern
787
- const extractPostHogProperties = extractSegmentProperties;
788
-
789
- // Helper function to extract Snowplow values from sphelp.NewString/NewFloat64
790
- function extractSnowplowValue(expr) {
791
- if (!expr) return null;
792
-
793
- // Direct value
794
- if (expr.tag === 'string') {
795
- return expr.value.slice(1, -1);
796
- }
797
- if (expr.tag === 'number') {
798
- return parseFloat(expr.value);
799
- }
800
-
801
- // Look for sphelp.NewString("value") or sphelp.NewFloat64(value)
802
- if (expr.tag === 'expr' && expr.body) {
803
- for (const item of expr.body) {
804
- if (item.tag === 'call' && item.func && item.func.tag === 'access') {
805
- if (item.func.member === 'NewString' && item.args && item.args.length > 0) {
806
- return extractStringValue(item.args[0]);
807
- }
808
- if (item.func.member === 'NewFloat64' && item.args && item.args.length > 0) {
809
- const numExpr = item.args[0];
810
- if (numExpr.tag === 'number') {
811
- return parseFloat(numExpr.value);
812
- }
813
- if (numExpr.tag === 'expr' && numExpr.body && numExpr.body[0] && numExpr.body[0].tag === 'number') {
814
- return parseFloat(numExpr.body[0].value);
815
- }
816
- }
817
- }
818
- }
819
- }
820
-
821
- return null;
822
- }
823
-
824
- function extractStringValue(node) {
825
- if (!node) return null;
826
-
827
- // Handle direct string literals
828
- if (node.tag === 'string') {
829
- // Remove quotes from the value
830
- return node.value.slice(1, -1);
831
- }
832
-
833
- // Handle expressions that might contain a string
834
- if (node.tag === 'expr' && node.body && node.body.length > 0) {
835
- // Look for string literals in the expression body
836
- for (const item of node.body) {
837
- if (item.tag === 'string') {
838
- return item.value.slice(1, -1);
839
- }
840
- }
841
- }
842
-
843
- return null;
844
- }
845
-
846
- function extractPropertiesFromExpr(expr, properties, source, typeContext, currentFunction) {
847
- // Handle struct literals (e.g., Type{field: value})
848
- if (expr.tag === 'structlit' && expr.fields) {
849
- for (const field of expr.fields) {
850
- if (field.name) {
851
- const propInfo = getPropertyInfo(field.value, typeContext, currentFunction);
852
- properties[field.name] = propInfo;
853
- } else if (field.value && field.value.tag === 'expr' && field.value.body) {
854
- // Handle map literal fields that don't have explicit names
855
- // Format: "key": value
856
- const keyNode = field.value.body[0];
857
- const colonNode = field.value.body[1];
858
- const valueNode = field.value.body[2];
859
-
860
- if (keyNode && keyNode.tag === 'string' && colonNode && colonNode.value === ':') {
861
- const key = keyNode.value.slice(1, -1); // Remove quotes
862
-
863
- // For nested maps, the value might include the map type declaration AND the structlit
864
- if (valueNode && valueNode.tag === 'index' && valueNode.container && valueNode.container.value === 'map') {
865
- // Look for the structlit that follows in the body
866
- const remainingNodes = field.value.body.slice(3); // Skip key, :, and map declaration
867
- const structlit = remainingNodes.find(node => node.tag === 'structlit');
868
- if (structlit) {
869
- properties[key] = getPropertyInfo(structlit, typeContext, currentFunction);
870
- } else {
871
- properties[key] = { type: 'object', properties: {} };
872
- }
873
- } else if (valueNode) {
874
- properties[key] = getPropertyInfo(valueNode, typeContext, currentFunction);
875
- }
876
- }
877
- }
878
- }
879
- }
880
-
881
- // Handle expressions that might contain a composite literal
882
- if (expr.tag === 'expr' && expr.body) {
883
- for (const item of expr.body) {
884
- if (item.tag === 'structlit') {
885
- extractPropertiesFromExpr(item, properties, source, typeContext, currentFunction);
886
- } else if (item.tag === 'index' && item.container && item.container.value === 'map') {
887
- // This is a map[string]interface{} type declaration
888
- // Look for the following structlit
889
- continue;
890
- }
891
- }
892
- }
893
- }
894
-
895
- function getPropertyInfo(value, typeContext, currentFunction) {
896
- if (!value) return { type: 'any' };
897
-
898
- // Handle direct values
899
- if (value.tag === 'string') {
900
- return { type: 'string' };
901
- }
902
-
903
- if (value.tag === 'number') {
904
- return { type: 'number' };
905
- }
906
-
907
- if (value.tag === 'ident') {
908
- // Check for boolean constants
909
- if (value.value === 'true' || value.value === 'false') {
910
- return { type: 'boolean' };
911
- }
912
- if (value.value === 'nil') {
913
- return { type: 'null' };
914
- }
915
- // Look up the variable type in the context
916
- const varName = value.value;
917
- let varInfo = null;
918
-
919
- // Check function parameters first
920
- if (typeContext && currentFunction && typeContext.functions[currentFunction]) {
921
- const funcContext = typeContext.functions[currentFunction];
922
- if (funcContext.params[varName]) {
923
- varInfo = funcContext.params[varName];
924
- } else if (funcContext.locals[varName]) {
925
- varInfo = funcContext.locals[varName];
926
- }
927
- }
928
-
929
- // Check global variables
930
- if (!varInfo && typeContext && typeContext.globals[varName]) {
931
- varInfo = typeContext.globals[varName];
932
- }
933
-
934
- if (varInfo) {
935
- // If we have a value stored for this variable, analyze it to get nested properties
936
- if (varInfo.value && varInfo.type && varInfo.type.tag === 'map') {
937
- // The variable has a map type and a value, extract properties from the value
938
- const nestedProps = {};
939
- extractPropertiesFromExpr(varInfo.value, nestedProps, null, typeContext, currentFunction);
940
- return {
941
- type: 'object',
942
- properties: nestedProps
943
- };
944
- }
945
- // Otherwise just return the type
946
- return mapGoTypeToSchemaType(varInfo.type);
947
- }
948
-
949
- // Otherwise it's an unknown variable reference
950
- return { type: 'any' };
951
- }
952
-
953
- // Handle index nodes (map[string]interface{})
954
- if (value.tag === 'index' && value.container && value.container.value === 'map') {
955
- // This indicates the start of a map literal, look for following structlit
956
- return { type: 'object', properties: {} };
957
- }
958
-
959
- // Handle expressions
960
- if (value.tag === 'expr' && value.body && value.body.length > 0) {
961
- const firstItem = value.body[0];
962
-
963
- // Check for literals
964
- if (firstItem.tag === 'string') return { type: 'string' };
965
- if (firstItem.tag === 'number') return { type: 'number' };
966
- if (firstItem.tag === 'ident') {
967
- if (firstItem.value === 'true' || firstItem.value === 'false') return { type: 'boolean' };
968
- if (firstItem.value === 'nil') return { type: 'null' };
969
- }
970
-
971
- // Check for array literals
972
- if (firstItem.tag === 'arraylit') {
973
- return {
974
- type: 'array',
975
- items: { type: 'any' }
976
- };
977
- }
978
-
979
- // Check for map declarations followed by struct literals
980
- if (firstItem.tag === 'index' && firstItem.container && firstItem.container.value === 'map') {
981
- // Look for the structlit that follows
982
- const structlit = value.body.find(item => item.tag === 'structlit');
983
- if (structlit && structlit.fields) {
984
- const nestedProps = {};
985
- // Inline the property extraction for nested objects
986
- for (const field of structlit.fields) {
987
- if (field.name) {
988
- nestedProps[field.name] = getPropertyInfo(field.value, typeContext, currentFunction);
989
- } else if (field.value && field.value.tag === 'expr' && field.value.body) {
990
- // Handle map literal fields
991
- const keyNode = field.value.body[0];
992
- const colonNode = field.value.body[1];
993
- const valueNode = field.value.body[2];
994
-
995
- if (keyNode && keyNode.tag === 'string' && colonNode && colonNode.value === ':') {
996
- const key = keyNode.value.slice(1, -1);
997
-
998
- // For nested maps, handle map declaration followed by structlit
999
- if (valueNode && valueNode.tag === 'index' && valueNode.container && valueNode.container.value === 'map') {
1000
- const remainingNodes = field.value.body.slice(3);
1001
- const structlit = remainingNodes.find(node => node.tag === 'structlit');
1002
- if (structlit) {
1003
- nestedProps[key] = getPropertyInfo(structlit, typeContext, currentFunction);
1004
- } else {
1005
- nestedProps[key] = { type: 'object', properties: {} };
1006
- }
1007
- } else if (valueNode) {
1008
- nestedProps[key] = getPropertyInfo(valueNode, typeContext, currentFunction);
1009
- }
1010
- }
1011
- }
1012
- }
1013
- return {
1014
- type: 'object',
1015
- properties: nestedProps
1016
- };
1017
- }
1018
- }
1019
-
1020
- // Check for struct/map literals
1021
- if (firstItem.tag === 'structlit') {
1022
- const nestedProps = {};
1023
- if (firstItem.fields) {
1024
- for (const field of firstItem.fields) {
1025
- if (field.name) {
1026
- nestedProps[field.name] = getPropertyInfo(field.value, typeContext, currentFunction);
1027
- } else if (field.value && field.value.tag === 'expr' && field.value.body) {
1028
- // Handle map literal fields
1029
- const keyNode = field.value.body[0];
1030
- const colonNode = field.value.body[1];
1031
- const valueNode = field.value.body[2];
1032
-
1033
- if (keyNode && keyNode.tag === 'string' && colonNode && colonNode.value === ':') {
1034
- const key = keyNode.value.slice(1, -1);
1035
-
1036
- // For nested maps, handle map declaration followed by structlit
1037
- if (valueNode && valueNode.tag === 'index' && valueNode.container && valueNode.container.value === 'map') {
1038
- const remainingNodes = field.value.body.slice(3);
1039
- const structlit = remainingNodes.find(node => node.tag === 'structlit');
1040
- if (structlit) {
1041
- nestedProps[key] = getPropertyInfo(structlit, typeContext, currentFunction);
1042
- } else {
1043
- nestedProps[key] = { type: 'object', properties: {} };
1044
- }
1045
- } else if (valueNode) {
1046
- nestedProps[key] = getPropertyInfo(valueNode, typeContext, currentFunction);
1047
- }
1048
- }
1049
- }
1050
- }
1051
- }
1052
- return {
1053
- type: 'object',
1054
- properties: nestedProps
1055
- };
1056
- }
1057
- }
1058
-
1059
- // Handle array literals
1060
- if (value.tag === 'arraylit') {
1061
- return {
1062
- type: 'array',
1063
- items: { type: 'any' }
1064
- };
1065
- }
1066
-
1067
- // Handle struct literals (nested objects)
1068
- if (value.tag === 'structlit') {
1069
- const nestedProps = {};
1070
- if (value.fields) {
1071
- for (const field of value.fields) {
1072
- if (field.name) {
1073
- nestedProps[field.name] = getPropertyInfo(field.value, typeContext, currentFunction);
1074
- } else if (field.value && field.value.tag === 'expr' && field.value.body) {
1075
- // Handle map literal fields
1076
- const keyNode = field.value.body[0];
1077
- const colonNode = field.value.body[1];
1078
- const valueNode = field.value.body[2];
1079
-
1080
- if (keyNode && keyNode.tag === 'string' && colonNode && colonNode.value === ':') {
1081
- const key = keyNode.value.slice(1, -1);
1082
-
1083
- // For nested maps, handle map declaration followed by structlit
1084
- if (valueNode && valueNode.tag === 'index' && valueNode.container && valueNode.container.value === 'map') {
1085
- const remainingNodes = field.value.body.slice(3);
1086
- const structlit = remainingNodes.find(node => node.tag === 'structlit');
1087
- if (structlit) {
1088
- nestedProps[key] = getPropertyInfo(structlit, typeContext, currentFunction);
1089
- } else {
1090
- nestedProps[key] = { type: 'object', properties: {} };
1091
- }
1092
- } else if (valueNode) {
1093
- nestedProps[key] = getPropertyInfo(valueNode, typeContext, currentFunction);
1094
- }
1095
- }
1096
- }
1097
- }
1098
- }
1099
- return {
1100
- type: 'object',
1101
- properties: nestedProps
1102
- };
1103
- }
1104
-
1105
- // Default to any type
1106
- return { type: 'any' };
1107
- }
1108
-
1109
- // Helper function to map Go types to schema types
1110
- function mapGoTypeToSchemaType(goType) {
1111
- if (!goType) return { type: 'any' };
1112
-
1113
- // Handle case where goType might be an object with a type property
1114
- if (goType.type) {
1115
- goType = goType.type;
1116
- }
1117
-
1118
- // Handle simple types
1119
- if (goType.tag === 'string') return { type: 'string' };
1120
- if (goType.tag === 'bool') return { type: 'boolean' };
1121
- if (goType.tag === 'int' || goType.tag === 'int8' || goType.tag === 'int16' ||
1122
- goType.tag === 'int32' || goType.tag === 'int64' || goType.tag === 'uint' ||
1123
- goType.tag === 'uint8' || goType.tag === 'uint16' || goType.tag === 'uint32' ||
1124
- goType.tag === 'uint64' || goType.tag === 'float32' || goType.tag === 'float64' ||
1125
- goType.tag === 'byte' || goType.tag === 'rune') {
1126
- return { type: 'number' };
1127
- }
1128
-
1129
- // Handle array types
1130
- if (goType.tag === 'array') {
1131
- const itemType = mapGoTypeToSchemaType(goType.item);
1132
- return {
1133
- type: 'array',
1134
- items: itemType
1135
- };
1136
- }
1137
-
1138
- // Handle slice types (arrays without fixed size)
1139
- if (goType.tag === 'array' && !goType.size) {
1140
- const itemType = mapGoTypeToSchemaType(goType.item);
1141
- return {
1142
- type: 'array',
1143
- items: itemType
1144
- };
1145
- }
1146
-
1147
- // Handle map types
1148
- if (goType.tag === 'map') {
1149
- return {
1150
- type: 'object',
1151
- properties: {}
1152
- };
1153
- }
1154
-
1155
- // Handle pointer types by dereferencing
1156
- if (goType.tag === 'ptr') {
1157
- return mapGoTypeToSchemaType(goType.item);
1158
- }
1159
-
1160
- // Default to any for complex or unknown types
1161
- return { type: 'any' };
1162
- }
1163
-
1164
- module.exports = { analyzeGoFile };