@flisk/analyze-tracking 0.7.0 → 0.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@flisk/analyze-tracking",
3
- "version": "0.7.0",
3
+ "version": "0.7.1",
4
4
  "description": "Analyzes tracking code in a project and generates data schemas",
5
5
  "main": "src/index.js",
6
6
  "bin": {
package/schema.json CHANGED
@@ -136,6 +136,10 @@
136
136
  "$ref": "#/definitions/property"
137
137
  }
138
138
  }
139
+ },
140
+ "items": {
141
+ "$ref": "#/definitions/property",
142
+ "description": "Schema for array items when type is 'array'"
139
143
  }
140
144
  },
141
145
  "required": [
@@ -10,6 +10,9 @@ async function analyzeGoFile(filePath, customFunction) {
10
10
  // Parse the Go file using go2json
11
11
  const ast = extractGoAST(source);
12
12
 
13
+ // First pass: build type information for functions and variables
14
+ const typeContext = buildTypeContext(ast);
15
+
13
16
  // Extract tracking events from the AST
14
17
  const events = [];
15
18
  let currentFunction = 'global';
@@ -20,7 +23,7 @@ async function analyzeGoFile(filePath, customFunction) {
20
23
  currentFunction = node.name;
21
24
  // Process the function body
22
25
  if (node.body) {
23
- extractEventsFromBody(node.body, events, filePath, currentFunction, customFunction);
26
+ extractEventsFromBody(node.body, events, filePath, currentFunction, customFunction, typeContext, currentFunction);
24
27
  }
25
28
  }
26
29
  }
@@ -60,43 +63,112 @@ async function analyzeGoFile(filePath, customFunction) {
60
63
  }
61
64
  }
62
65
 
63
- function extractEventsFromBody(body, events, filePath, functionName, customFunction) {
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) {
64
136
  for (const stmt of body) {
65
137
  if (stmt.tag === 'exec' && stmt.expr) {
66
- processExpression(stmt.expr, events, filePath, functionName, customFunction);
138
+ processExpression(stmt.expr, events, filePath, functionName, customFunction, typeContext, currentFunction);
67
139
  } else if (stmt.tag === 'declare' && stmt.value) {
68
140
  // Handle variable declarations with tracking calls
69
- processExpression(stmt.value, events, filePath, functionName, customFunction);
141
+ processExpression(stmt.value, events, filePath, functionName, customFunction, typeContext, currentFunction);
70
142
  } else if (stmt.tag === 'assign' && stmt.rhs) {
71
143
  // Handle assignments with tracking calls
72
- processExpression(stmt.rhs, events, filePath, functionName, customFunction);
144
+ processExpression(stmt.rhs, events, filePath, functionName, customFunction, typeContext, currentFunction);
73
145
  } else if (stmt.tag === 'if' && stmt.body) {
74
- extractEventsFromBody(stmt.body, events, filePath, functionName, customFunction);
146
+ extractEventsFromBody(stmt.body, events, filePath, functionName, customFunction, typeContext, currentFunction);
75
147
  } else if (stmt.tag === 'elseif' && stmt.body) {
76
- extractEventsFromBody(stmt.body, events, filePath, functionName, customFunction);
148
+ extractEventsFromBody(stmt.body, events, filePath, functionName, customFunction, typeContext, currentFunction);
77
149
  } else if (stmt.tag === 'else' && stmt.body) {
78
- extractEventsFromBody(stmt.body, events, filePath, functionName, customFunction);
150
+ extractEventsFromBody(stmt.body, events, filePath, functionName, customFunction, typeContext, currentFunction);
79
151
  } else if (stmt.tag === 'for' && stmt.body) {
80
- extractEventsFromBody(stmt.body, events, filePath, functionName, customFunction);
152
+ extractEventsFromBody(stmt.body, events, filePath, functionName, customFunction, typeContext, currentFunction);
81
153
  } else if (stmt.tag === 'foreach' && stmt.body) {
82
- extractEventsFromBody(stmt.body, events, filePath, functionName, customFunction);
154
+ extractEventsFromBody(stmt.body, events, filePath, functionName, customFunction, typeContext, currentFunction);
83
155
  } else if (stmt.tag === 'switch' && stmt.cases) {
84
156
  for (const caseNode of stmt.cases) {
85
157
  if (caseNode.body) {
86
- extractEventsFromBody(caseNode.body, events, filePath, functionName, customFunction);
158
+ extractEventsFromBody(caseNode.body, events, filePath, functionName, customFunction, typeContext, currentFunction);
87
159
  }
88
160
  }
89
161
  }
90
162
  }
91
163
  }
92
164
 
93
- function processExpression(expr, events, filePath, functionName, customFunction, depth = 0) {
165
+ function processExpression(expr, events, filePath, functionName, customFunction, typeContext, currentFunction, depth = 0) {
94
166
  if (!expr || depth > 20) return; // Prevent infinite recursion with depth limit
95
167
 
96
168
  // Handle array of expressions
97
169
  if (Array.isArray(expr)) {
98
170
  for (const item of expr) {
99
- processExpression(item, events, filePath, functionName, customFunction, depth + 1);
171
+ processExpression(item, events, filePath, functionName, customFunction, typeContext, currentFunction, depth + 1);
100
172
  }
101
173
  return;
102
174
  }
@@ -104,25 +176,25 @@ function processExpression(expr, events, filePath, functionName, customFunction,
104
176
  // Handle single expression with body
105
177
  if (expr.body) {
106
178
  for (const item of expr.body) {
107
- processExpression(item, events, filePath, functionName, customFunction, depth + 1);
179
+ processExpression(item, events, filePath, functionName, customFunction, typeContext, currentFunction, depth + 1);
108
180
  }
109
181
  return;
110
182
  }
111
183
 
112
184
  // Handle specific node types
113
185
  if (expr.tag === 'call') {
114
- const trackingCall = extractTrackingCall(expr, filePath, functionName, customFunction);
186
+ const trackingCall = extractTrackingCall(expr, filePath, functionName, customFunction, typeContext, currentFunction);
115
187
  if (trackingCall) {
116
188
  events.push(trackingCall);
117
189
  }
118
190
 
119
191
  // Also process call arguments
120
192
  if (expr.args) {
121
- processExpression(expr.args, events, filePath, functionName, customFunction, depth + 1);
193
+ processExpression(expr.args, events, filePath, functionName, customFunction, typeContext, currentFunction, depth + 1);
122
194
  }
123
195
  } else if (expr.tag === 'structlit') {
124
196
  // Check if this struct literal is a tracking event
125
- const trackingCall = extractTrackingCall(expr, filePath, functionName, customFunction);
197
+ const trackingCall = extractTrackingCall(expr, filePath, functionName, customFunction, typeContext, currentFunction);
126
198
  if (trackingCall) {
127
199
  events.push(trackingCall);
128
200
  }
@@ -131,7 +203,7 @@ function processExpression(expr, events, filePath, functionName, customFunction,
131
203
  if (!trackingCall && expr.fields) {
132
204
  for (const field of expr.fields) {
133
205
  if (field.value) {
134
- processExpression(field.value, events, filePath, functionName, customFunction, depth + 1);
206
+ processExpression(field.value, events, filePath, functionName, customFunction, typeContext, currentFunction, depth + 1);
135
207
  }
136
208
  }
137
209
  }
@@ -139,24 +211,24 @@ function processExpression(expr, events, filePath, functionName, customFunction,
139
211
 
140
212
  // Process other common properties that might contain expressions
141
213
  if (expr.value && expr.tag !== 'structlit') {
142
- processExpression(expr.value, events, filePath, functionName, customFunction, depth + 1);
214
+ processExpression(expr.value, events, filePath, functionName, customFunction, typeContext, currentFunction, depth + 1);
143
215
  }
144
216
  if (expr.lhs) {
145
- processExpression(expr.lhs, events, filePath, functionName, customFunction, depth + 1);
217
+ processExpression(expr.lhs, events, filePath, functionName, customFunction, typeContext, currentFunction, depth + 1);
146
218
  }
147
219
  if (expr.rhs) {
148
- processExpression(expr.rhs, events, filePath, functionName, customFunction, depth + 1);
220
+ processExpression(expr.rhs, events, filePath, functionName, customFunction, typeContext, currentFunction, depth + 1);
149
221
  }
150
222
  }
151
223
 
152
- function extractTrackingCall(callNode, filePath, functionName, customFunction) {
224
+ function extractTrackingCall(callNode, filePath, functionName, customFunction, typeContext, currentFunction) {
153
225
  const source = detectSource(callNode, customFunction);
154
226
  if (!source) return null;
155
227
 
156
228
  const eventName = extractEventName(callNode, source);
157
229
  if (!eventName) return null;
158
230
 
159
- const properties = extractProperties(callNode, source);
231
+ const properties = extractProperties(callNode, source, typeContext, currentFunction);
160
232
 
161
233
  // Get line number based on source type
162
234
  let line = 0;
@@ -329,12 +401,12 @@ function extractEventName(callNode, source) {
329
401
  return null;
330
402
  }
331
403
 
332
- function extractProperties(callNode, source) {
404
+ function extractProperties(callNode, source, typeContext, currentFunction) {
333
405
  const properties = {};
334
406
 
335
407
  switch (source) {
336
408
  case 'mixpanel':
337
- // mp.Track(ctx, []*mixpanel.Event{mp.NewEvent("event", "", map[string]any{...})})
409
+ // mp.Track(ctx, []*mixpanel.Event{mp.NewEvent("event", "distinctId", map[string]any{...})})
338
410
  if (callNode.args && callNode.args.length > 1) {
339
411
  const arrayArg = callNode.args[1];
340
412
  if (arrayArg.tag === 'expr' && arrayArg.body) {
@@ -342,19 +414,48 @@ function extractProperties(callNode, source) {
342
414
  if (arrayLit && arrayLit.items && arrayLit.items.length > 0) {
343
415
  const firstItem = arrayLit.items[0];
344
416
  if (Array.isArray(firstItem)) {
345
- // Look for pattern: mp.NewEvent("event", "", map[string]any{...})
346
- // The third argument is the properties map
417
+ // Look for pattern: mp.NewEvent("event", "distinctId", map[string]any{...})
347
418
  let foundNewEvent = false;
348
419
  for (let i = 0; i < firstItem.length - 4; i++) {
349
420
  if (firstItem[i].tag === 'ident' && firstItem[i].value === 'mp' &&
350
421
  firstItem[i+1].tag === 'sigil' && firstItem[i+1].value === '.' &&
351
422
  firstItem[i+2].tag === 'ident' && firstItem[i+2].value === 'NewEvent' &&
352
423
  firstItem[i+3].tag === 'sigil' && firstItem[i+3].value === '(') {
353
- // Found mp.NewEvent( - skip event name and empty string to find the map
424
+ // Found mp.NewEvent( - process arguments
354
425
  let j = i + 4;
355
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
+ }
356
457
 
357
- // Skip to the third argument (after 2 commas)
458
+ // Continue to find the properties map (third argument)
358
459
  while (j < firstItem.length && commaCount < 2) {
359
460
  if (firstItem[j].tag === 'sigil' && firstItem[j].value === ',') {
360
461
  commaCount++;
@@ -370,13 +471,15 @@ function extractProperties(callNode, source) {
370
471
  while (j < firstItem.length) {
371
472
  if (firstItem[j].tag === 'sigil' && firstItem[j].value === '{') {
372
473
  // Simple property extraction from tokens
373
- // Look for pattern: "key": "value"
474
+ // Look for pattern: "key": value
374
475
  for (let k = j + 1; k < firstItem.length - 2; k++) {
375
476
  if (firstItem[k].tag === 'string' &&
376
- firstItem[k+1].tag === 'sigil' && firstItem[k+1].value === ':' &&
377
- firstItem[k+2].tag === 'string') {
477
+ firstItem[k+1].tag === 'sigil' && firstItem[k+1].value === ':') {
378
478
  const key = firstItem[k].value.slice(1, -1);
379
- properties[key] = { type: 'string' };
479
+
480
+ // Use getPropertyInfo to determine the type
481
+ const valueToken = firstItem[k+2];
482
+ properties[key] = getPropertyInfo(valueToken, typeContext, currentFunction);
380
483
  }
381
484
  }
382
485
  foundNewEvent = true;
@@ -405,16 +508,16 @@ function extractProperties(callNode, source) {
405
508
  // Extract UserId/DistinctId
406
509
  const idField = findStructField(callNode, source === 'segment' ? 'UserId' : 'DistinctId');
407
510
  if (idField) {
408
- properties[source === 'segment' ? 'userId' : 'distinctId'] = { type: 'string' };
511
+ properties[source === 'segment' ? 'UserId' : 'DistinctId'] = { type: 'string' };
409
512
  }
410
513
 
411
514
  // Extract Properties
412
515
  const propsField = findStructField(callNode, 'Properties');
413
516
  if (propsField && propsField.value) {
414
517
  if (source === 'segment') {
415
- extractSegmentProperties(propsField.value, properties);
518
+ extractSegmentProperties(propsField.value, properties, typeContext, currentFunction);
416
519
  } else {
417
- extractPostHogProperties(propsField.value, properties);
520
+ extractPostHogProperties(propsField.value, properties, typeContext, currentFunction);
418
521
  }
419
522
  }
420
523
  }
@@ -426,13 +529,13 @@ function extractProperties(callNode, source) {
426
529
  // Extract UserID
427
530
  const userIdField = findStructField(callNode, 'UserID');
428
531
  if (userIdField) {
429
- properties.userId = { type: 'string' };
532
+ properties['UserID'] = { type: 'string' };
430
533
  }
431
534
 
432
535
  // Extract EventProperties
433
536
  const eventPropsField = findStructField(callNode, 'EventProperties');
434
537
  if (eventPropsField) {
435
- extractPropertiesFromExpr(eventPropsField.value, properties, source);
538
+ extractPropertiesFromExpr(eventPropsField.value, properties, source, typeContext, currentFunction);
436
539
  }
437
540
 
438
541
  // Extract EventOptions
@@ -456,7 +559,7 @@ function extractProperties(callNode, source) {
456
559
  if (value.tag === 'number') {
457
560
  properties[fieldName] = { type: 'number' };
458
561
  } else {
459
- properties[fieldName] = getPropertyInfo(value);
562
+ properties[fieldName] = getPropertyInfo(value, typeContext, currentFunction);
460
563
  }
461
564
  }
462
565
  }
@@ -472,13 +575,13 @@ function extractProperties(callNode, source) {
472
575
  // Extract UserID
473
576
  const userIdField = findStructField(eventStruct, 'UserID');
474
577
  if (userIdField) {
475
- properties.userId = { type: 'string' };
578
+ properties['UserID'] = { type: 'string' };
476
579
  }
477
580
 
478
581
  // Extract EventProperties
479
582
  const eventPropsField = findStructField(eventStruct, 'EventProperties');
480
583
  if (eventPropsField) {
481
- extractPropertiesFromExpr(eventPropsField.value, properties, source);
584
+ extractPropertiesFromExpr(eventPropsField.value, properties, source, typeContext, currentFunction);
482
585
  }
483
586
 
484
587
  // Extract EventOptions
@@ -502,7 +605,7 @@ function extractProperties(callNode, source) {
502
605
  if (value.tag === 'number') {
503
606
  properties[fieldName] = { type: 'number' };
504
607
  } else {
505
- properties[fieldName] = getPropertyInfo(value);
608
+ properties[fieldName] = getPropertyInfo(value, typeContext, currentFunction);
506
609
  }
507
610
  }
508
611
  }
@@ -523,9 +626,33 @@ function extractProperties(callNode, source) {
523
626
  for (const field of structEvent.fields) {
524
627
  const fieldName = extractFieldName(field);
525
628
  if (fieldName && fieldName !== 'Action') {
526
- const value = extractSnowplowValue(field.value);
527
- if (value !== null) {
528
- properties[fieldName] = { type: typeof value === 'number' ? 'number' : 'string' };
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
+ }
529
656
  }
530
657
  }
531
658
  }
@@ -536,7 +663,7 @@ function extractProperties(callNode, source) {
536
663
  case 'custom':
537
664
  // customFunction("event", map[string]interface{}{...})
538
665
  if (callNode.args && callNode.args.length > 1) {
539
- extractPropertiesFromExpr(callNode.args[1], properties, source);
666
+ extractPropertiesFromExpr(callNode.args[1], properties, source, typeContext, currentFunction);
540
667
  }
541
668
  break;
542
669
  }
@@ -598,7 +725,7 @@ function extractFieldName(field) {
598
725
  }
599
726
 
600
727
  // Helper function to extract Segment/PostHog properties from NewProperties().Set() chain
601
- function extractSegmentProperties(expr, properties) {
728
+ function extractSegmentProperties(expr, properties, typeContext, currentFunction) {
602
729
  if (!expr) return;
603
730
 
604
731
  // Look for method calls in the expression
@@ -628,19 +755,9 @@ function extractSegmentProperties(expr, properties) {
628
755
  // Handle different value types
629
756
  if (value.tag === 'expr' && value.body) {
630
757
  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
- }
758
+ properties[key] = getPropertyInfo(firstItem, typeContext, currentFunction);
759
+ } else {
760
+ properties[key] = getPropertyInfo(value, typeContext, currentFunction);
644
761
  }
645
762
  }
646
763
  }
@@ -654,19 +771,9 @@ function extractSegmentProperties(expr, properties) {
654
771
  // Handle different value types
655
772
  if (value.tag === 'expr' && value.body) {
656
773
  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
- }
774
+ properties[key] = getPropertyInfo(firstItem, typeContext, currentFunction);
775
+ } else {
776
+ properties[key] = getPropertyInfo(value, typeContext, currentFunction);
670
777
  }
671
778
  }
672
779
  }
@@ -736,12 +843,12 @@ function extractStringValue(node) {
736
843
  return null;
737
844
  }
738
845
 
739
- function extractPropertiesFromExpr(expr, properties, source) {
846
+ function extractPropertiesFromExpr(expr, properties, source, typeContext, currentFunction) {
740
847
  // Handle struct literals (e.g., Type{field: value})
741
848
  if (expr.tag === 'structlit' && expr.fields) {
742
849
  for (const field of expr.fields) {
743
850
  if (field.name) {
744
- const propInfo = getPropertyInfo(field.value);
851
+ const propInfo = getPropertyInfo(field.value, typeContext, currentFunction);
745
852
  properties[field.name] = propInfo;
746
853
  } else if (field.value && field.value.tag === 'expr' && field.value.body) {
747
854
  // Handle map literal fields that don't have explicit names
@@ -759,12 +866,12 @@ function extractPropertiesFromExpr(expr, properties, source) {
759
866
  const remainingNodes = field.value.body.slice(3); // Skip key, :, and map declaration
760
867
  const structlit = remainingNodes.find(node => node.tag === 'structlit');
761
868
  if (structlit) {
762
- properties[key] = getPropertyInfo(structlit);
869
+ properties[key] = getPropertyInfo(structlit, typeContext, currentFunction);
763
870
  } else {
764
871
  properties[key] = { type: 'object', properties: {} };
765
872
  }
766
873
  } else if (valueNode) {
767
- properties[key] = getPropertyInfo(valueNode);
874
+ properties[key] = getPropertyInfo(valueNode, typeContext, currentFunction);
768
875
  }
769
876
  }
770
877
  }
@@ -775,7 +882,7 @@ function extractPropertiesFromExpr(expr, properties, source) {
775
882
  if (expr.tag === 'expr' && expr.body) {
776
883
  for (const item of expr.body) {
777
884
  if (item.tag === 'structlit') {
778
- extractPropertiesFromExpr(item, properties, source);
885
+ extractPropertiesFromExpr(item, properties, source, typeContext, currentFunction);
779
886
  } else if (item.tag === 'index' && item.container && item.container.value === 'map') {
780
887
  // This is a map[string]interface{} type declaration
781
888
  // Look for the following structlit
@@ -785,7 +892,7 @@ function extractPropertiesFromExpr(expr, properties, source) {
785
892
  }
786
893
  }
787
894
 
788
- function getPropertyInfo(value) {
895
+ function getPropertyInfo(value, typeContext, currentFunction) {
789
896
  if (!value) return { type: 'any' };
790
897
 
791
898
  // Handle direct values
@@ -805,7 +912,41 @@ function getPropertyInfo(value) {
805
912
  if (value.value === 'nil') {
806
913
  return { type: 'null' };
807
914
  }
808
- // Otherwise it's a variable reference
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
809
950
  return { type: 'any' };
810
951
  }
811
952
 
@@ -844,7 +985,7 @@ function getPropertyInfo(value) {
844
985
  // Inline the property extraction for nested objects
845
986
  for (const field of structlit.fields) {
846
987
  if (field.name) {
847
- nestedProps[field.name] = getPropertyInfo(field.value);
988
+ nestedProps[field.name] = getPropertyInfo(field.value, typeContext, currentFunction);
848
989
  } else if (field.value && field.value.tag === 'expr' && field.value.body) {
849
990
  // Handle map literal fields
850
991
  const keyNode = field.value.body[0];
@@ -859,12 +1000,12 @@ function getPropertyInfo(value) {
859
1000
  const remainingNodes = field.value.body.slice(3);
860
1001
  const structlit = remainingNodes.find(node => node.tag === 'structlit');
861
1002
  if (structlit) {
862
- nestedProps[key] = getPropertyInfo(structlit);
1003
+ nestedProps[key] = getPropertyInfo(structlit, typeContext, currentFunction);
863
1004
  } else {
864
1005
  nestedProps[key] = { type: 'object', properties: {} };
865
1006
  }
866
1007
  } else if (valueNode) {
867
- nestedProps[key] = getPropertyInfo(valueNode);
1008
+ nestedProps[key] = getPropertyInfo(valueNode, typeContext, currentFunction);
868
1009
  }
869
1010
  }
870
1011
  }
@@ -882,7 +1023,7 @@ function getPropertyInfo(value) {
882
1023
  if (firstItem.fields) {
883
1024
  for (const field of firstItem.fields) {
884
1025
  if (field.name) {
885
- nestedProps[field.name] = getPropertyInfo(field.value);
1026
+ nestedProps[field.name] = getPropertyInfo(field.value, typeContext, currentFunction);
886
1027
  } else if (field.value && field.value.tag === 'expr' && field.value.body) {
887
1028
  // Handle map literal fields
888
1029
  const keyNode = field.value.body[0];
@@ -897,12 +1038,12 @@ function getPropertyInfo(value) {
897
1038
  const remainingNodes = field.value.body.slice(3);
898
1039
  const structlit = remainingNodes.find(node => node.tag === 'structlit');
899
1040
  if (structlit) {
900
- nestedProps[key] = getPropertyInfo(structlit);
1041
+ nestedProps[key] = getPropertyInfo(structlit, typeContext, currentFunction);
901
1042
  } else {
902
1043
  nestedProps[key] = { type: 'object', properties: {} };
903
1044
  }
904
1045
  } else if (valueNode) {
905
- nestedProps[key] = getPropertyInfo(valueNode);
1046
+ nestedProps[key] = getPropertyInfo(valueNode, typeContext, currentFunction);
906
1047
  }
907
1048
  }
908
1049
  }
@@ -929,7 +1070,7 @@ function getPropertyInfo(value) {
929
1070
  if (value.fields) {
930
1071
  for (const field of value.fields) {
931
1072
  if (field.name) {
932
- nestedProps[field.name] = getPropertyInfo(field.value);
1073
+ nestedProps[field.name] = getPropertyInfo(field.value, typeContext, currentFunction);
933
1074
  } else if (field.value && field.value.tag === 'expr' && field.value.body) {
934
1075
  // Handle map literal fields
935
1076
  const keyNode = field.value.body[0];
@@ -944,12 +1085,12 @@ function getPropertyInfo(value) {
944
1085
  const remainingNodes = field.value.body.slice(3);
945
1086
  const structlit = remainingNodes.find(node => node.tag === 'structlit');
946
1087
  if (structlit) {
947
- nestedProps[key] = getPropertyInfo(structlit);
1088
+ nestedProps[key] = getPropertyInfo(structlit, typeContext, currentFunction);
948
1089
  } else {
949
1090
  nestedProps[key] = { type: 'object', properties: {} };
950
1091
  }
951
1092
  } else if (valueNode) {
952
- nestedProps[key] = getPropertyInfo(valueNode);
1093
+ nestedProps[key] = getPropertyInfo(valueNode, typeContext, currentFunction);
953
1094
  }
954
1095
  }
955
1096
  }
@@ -965,4 +1106,59 @@ function getPropertyInfo(value) {
965
1106
  return { type: 'any' };
966
1107
  }
967
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
+
968
1164
  module.exports = { analyzeGoFile };