@flisk/analyze-tracking 0.7.2 → 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 (64) hide show
  1. package/bin/cli.js +1 -1
  2. package/package.json +9 -7
  3. package/src/analyze/go/astTraversal.js +121 -0
  4. package/src/analyze/go/constants.js +20 -0
  5. package/src/analyze/go/eventDeduplicator.js +47 -0
  6. package/src/analyze/go/eventExtractor.js +156 -0
  7. package/src/analyze/go/goAstParser/constants.js +39 -0
  8. package/src/analyze/go/goAstParser/expressionParser.js +281 -0
  9. package/src/analyze/go/goAstParser/index.js +52 -0
  10. package/src/analyze/go/goAstParser/statementParser.js +387 -0
  11. package/src/analyze/go/goAstParser/tokenizer.js +196 -0
  12. package/src/analyze/go/goAstParser/typeParser.js +202 -0
  13. package/src/analyze/go/goAstParser/utils.js +99 -0
  14. package/src/analyze/go/index.js +55 -0
  15. package/src/analyze/go/propertyExtractor.js +670 -0
  16. package/src/analyze/go/trackingDetector.js +71 -0
  17. package/src/analyze/go/trackingExtractor.js +54 -0
  18. package/src/analyze/go/typeContext.js +88 -0
  19. package/src/analyze/go/utils.js +215 -0
  20. package/src/analyze/index.js +11 -6
  21. package/src/analyze/javascript/constants.js +115 -0
  22. package/src/analyze/javascript/detectors/analytics-source.js +119 -0
  23. package/src/analyze/javascript/detectors/index.js +10 -0
  24. package/src/analyze/javascript/extractors/event-extractor.js +179 -0
  25. package/src/analyze/javascript/extractors/index.js +13 -0
  26. package/src/analyze/javascript/extractors/property-extractor.js +172 -0
  27. package/src/analyze/javascript/index.js +38 -0
  28. package/src/analyze/javascript/parser.js +126 -0
  29. package/src/analyze/javascript/utils/function-finder.js +123 -0
  30. package/src/analyze/python/index.js +111 -0
  31. package/src/analyze/python/pythonTrackingAnalyzer.py +814 -0
  32. package/src/analyze/ruby/detectors.js +46 -0
  33. package/src/analyze/ruby/extractors.js +258 -0
  34. package/src/analyze/ruby/index.js +51 -0
  35. package/src/analyze/ruby/traversal.js +123 -0
  36. package/src/analyze/ruby/types.js +30 -0
  37. package/src/analyze/ruby/visitor.js +66 -0
  38. package/src/analyze/typescript/constants.js +109 -0
  39. package/src/analyze/typescript/detectors/analytics-source.js +120 -0
  40. package/src/analyze/typescript/detectors/index.js +10 -0
  41. package/src/analyze/typescript/extractors/event-extractor.js +269 -0
  42. package/src/analyze/typescript/extractors/index.js +14 -0
  43. package/src/analyze/typescript/extractors/property-extractor.js +395 -0
  44. package/src/analyze/typescript/index.js +48 -0
  45. package/src/analyze/typescript/parser.js +131 -0
  46. package/src/analyze/typescript/utils/function-finder.js +114 -0
  47. package/src/analyze/typescript/utils/type-resolver.js +193 -0
  48. package/src/generateDescriptions/index.js +81 -0
  49. package/src/generateDescriptions/llmUtils.js +33 -0
  50. package/src/generateDescriptions/promptUtils.js +62 -0
  51. package/src/generateDescriptions/schemaUtils.js +61 -0
  52. package/src/index.js +7 -2
  53. package/src/{fileProcessor.js → utils/fileProcessor.js} +5 -0
  54. package/src/{repoDetails.js → utils/repoDetails.js} +5 -0
  55. package/src/{yamlGenerator.js → utils/yamlGenerator.js} +5 -0
  56. package/src/analyze/analyzeGoFile.js +0 -1164
  57. package/src/analyze/analyzeJsFile.js +0 -87
  58. package/src/analyze/analyzePythonFile.js +0 -42
  59. package/src/analyze/analyzeRubyFile.js +0 -419
  60. package/src/analyze/analyzeTsFile.js +0 -192
  61. package/src/analyze/go2json.js +0 -1069
  62. package/src/analyze/helpers.js +0 -656
  63. package/src/analyze/pythonTrackingAnalyzer.py +0 -541
  64. package/src/generateDescriptions.js +0 -196
@@ -0,0 +1,670 @@
1
+ /**
2
+ * @fileoverview Property extraction utilities for Go analytics tracking
3
+ * @module analyze/go/propertyExtractor
4
+ */
5
+
6
+ const { ANALYTICS_SOURCES } = require('./constants');
7
+ const { extractStringValue, findStructLiteral, findStructField, extractFieldName, mapGoTypeToSchemaType } = require('./utils');
8
+
9
+ /**
10
+ * Extract properties from a tracking call based on the source
11
+ * @param {Object} callNode - AST node representing a function call or struct literal
12
+ * @param {string} source - Analytics source (e.g., 'segment', 'amplitude')
13
+ * @param {Object} typeContext - Type information context for variable resolution
14
+ * @param {string} currentFunction - Current function context for type lookups
15
+ * @returns {Object} Object containing extracted properties with their type information
16
+ */
17
+ function extractProperties(callNode, source, typeContext, currentFunction) {
18
+ const properties = {};
19
+
20
+ switch (source) {
21
+ case ANALYTICS_SOURCES.MIXPANEL:
22
+ extractMixpanelProperties(callNode, properties, typeContext, currentFunction);
23
+ break;
24
+
25
+ case ANALYTICS_SOURCES.SEGMENT:
26
+ case ANALYTICS_SOURCES.POSTHOG:
27
+ extractSegmentPosthogProperties(callNode, properties, source, typeContext, currentFunction);
28
+ break;
29
+
30
+ case ANALYTICS_SOURCES.AMPLITUDE:
31
+ extractAmplitudeProperties(callNode, properties, typeContext, currentFunction);
32
+ break;
33
+
34
+ case ANALYTICS_SOURCES.SNOWPLOW:
35
+ extractSnowplowProperties(callNode, properties, typeContext, currentFunction);
36
+ break;
37
+
38
+ case ANALYTICS_SOURCES.CUSTOM:
39
+ extractCustomProperties(callNode, properties, typeContext, currentFunction);
40
+ break;
41
+ }
42
+
43
+ return properties;
44
+ }
45
+
46
+ /**
47
+ * Extract Mixpanel properties
48
+ * Pattern: mp.Track(ctx, []*mixpanel.Event{mp.NewEvent("event_name", "distinctId", map[string]any{...})})
49
+ * @param {Object} callNode - AST node for Mixpanel tracking call
50
+ * @param {Object} properties - Object to store extracted properties (modified in place)
51
+ * @param {Object} typeContext - Type information context for variable resolution
52
+ * @param {string} currentFunction - Current function context for type lookups
53
+ */
54
+ function extractMixpanelProperties(callNode, properties, typeContext, currentFunction) {
55
+ if (callNode.args && callNode.args.length > 1) {
56
+ const arrayArg = callNode.args[1];
57
+ if (arrayArg.tag === 'expr' && arrayArg.body) {
58
+ const arrayLit = arrayArg.body.find(item => item.tag === 'arraylit');
59
+ if (arrayLit && arrayLit.items && arrayLit.items.length > 0) {
60
+ const firstItem = arrayLit.items[0];
61
+ if (Array.isArray(firstItem)) {
62
+ // Look for mp.NewEvent pattern and extract properties
63
+ let foundNewEvent = false;
64
+ for (let i = 0; i < firstItem.length - 4; i++) {
65
+ if (firstItem[i].tag === 'ident' && firstItem[i].value === 'mp' &&
66
+ firstItem[i+1].tag === 'sigil' && firstItem[i+1].value === '.' &&
67
+ firstItem[i+2].tag === 'ident' && firstItem[i+2].value === 'NewEvent' &&
68
+ firstItem[i+3].tag === 'sigil' && firstItem[i+3].value === '(') {
69
+ // Found mp.NewEvent( - process arguments
70
+ let j = i + 4;
71
+ let commaCount = 0;
72
+
73
+ // Skip the first argument (event name)
74
+ while (j < firstItem.length && commaCount < 1) {
75
+ if (firstItem[j].tag === 'sigil' && firstItem[j].value === ',') {
76
+ commaCount++;
77
+ }
78
+ j++;
79
+ }
80
+
81
+ // Extract the second argument (DistinctId)
82
+ if (j < firstItem.length) {
83
+ // Skip whitespace
84
+ while (j < firstItem.length && firstItem[j].tag === 'newline') {
85
+ j++;
86
+ }
87
+
88
+ if (firstItem[j]) {
89
+ if (firstItem[j].tag === 'string') {
90
+ // It's a string literal
91
+ const distinctId = firstItem[j].value.slice(1, -1); // Remove quotes
92
+ if (distinctId !== '') { // Only add if not empty string
93
+ properties['DistinctId'] = { type: 'string' };
94
+ }
95
+ } else if (firstItem[j].tag === 'ident') {
96
+ // It's a variable reference - look up its type
97
+ properties['DistinctId'] = getPropertyInfo(firstItem[j], typeContext, currentFunction);
98
+ }
99
+ }
100
+ }
101
+
102
+ // Continue to find the properties map (third argument)
103
+ while (j < firstItem.length && commaCount < 2) {
104
+ if (firstItem[j].tag === 'sigil' && firstItem[j].value === ',') {
105
+ commaCount++;
106
+ }
107
+ j++;
108
+ }
109
+
110
+ // Look for map[string]any{ pattern
111
+ while (j < firstItem.length - 2) {
112
+ if (firstItem[j].tag === 'ident' && firstItem[j].value === 'map' &&
113
+ firstItem[j+1].tag === 'sigil' && firstItem[j+1].value === '[') {
114
+ // Found the start of the map, now look for the opening brace
115
+ while (j < firstItem.length) {
116
+ if (firstItem[j].tag === 'sigil' && firstItem[j].value === '{') {
117
+ // Simple property extraction from tokens
118
+ // Look for pattern: "key": value
119
+ for (let k = j + 1; k < firstItem.length - 2; k++) {
120
+ if (firstItem[k].tag === 'string' &&
121
+ firstItem[k+1].tag === 'sigil' && firstItem[k+1].value === ':') {
122
+ const key = firstItem[k].value.slice(1, -1);
123
+
124
+ // Use getPropertyInfo to determine the type
125
+ const valueToken = firstItem[k+2];
126
+ properties[key] = getPropertyInfo(valueToken, typeContext, currentFunction);
127
+ }
128
+ }
129
+ foundNewEvent = true;
130
+ break;
131
+ }
132
+ j++;
133
+ }
134
+ if (foundNewEvent) break;
135
+ }
136
+ j++;
137
+ }
138
+ if (foundNewEvent) break;
139
+ }
140
+ }
141
+ }
142
+ }
143
+ }
144
+ }
145
+ }
146
+
147
+ /**
148
+ * Extract Segment/PostHog properties
149
+ * Pattern: analytics.Track{UserId: "...", Properties: analytics.NewProperties().Set(...)}
150
+ * @param {Object} callNode - AST node for Segment/PostHog struct literal
151
+ * @param {Object} properties - Object to store extracted properties (modified in place)
152
+ * @param {string} source - Either 'segment' or 'posthog'
153
+ * @param {Object} typeContext - Type information context for variable resolution
154
+ * @param {string} currentFunction - Current function context for type lookups
155
+ */
156
+ function extractSegmentPosthogProperties(callNode, properties, source, typeContext, currentFunction) {
157
+ if (callNode.fields) {
158
+ // Extract UserId/DistinctId
159
+ const idField = findStructField(callNode, source === ANALYTICS_SOURCES.SEGMENT ? 'UserId' : 'DistinctId');
160
+ if (idField) {
161
+ properties[source === ANALYTICS_SOURCES.SEGMENT ? 'UserId' : 'DistinctId'] = { type: 'string' };
162
+ }
163
+
164
+ // Extract Properties
165
+ const propsField = findStructField(callNode, 'Properties');
166
+ if (propsField && propsField.value) {
167
+ extractChainedProperties(propsField.value, properties, typeContext, currentFunction);
168
+ }
169
+ }
170
+ }
171
+
172
+ /**
173
+ * Extract Amplitude properties
174
+ * Pattern: amplitude.Event{UserID: "...", EventProperties: map[string]interface{}{...}}
175
+ * @param {Object} callNode - AST node for Amplitude tracking call
176
+ * @param {Object} properties - Object to store extracted properties (modified in place)
177
+ * @param {Object} typeContext - Type information context for variable resolution
178
+ * @param {string} currentFunction - Current function context for type lookups
179
+ */
180
+ function extractAmplitudeProperties(callNode, properties, typeContext, currentFunction) {
181
+ let fields = null;
182
+
183
+ // For struct literals: amplitude.Event{UserID: "...", EventProperties: map[string]interface{}{...}}
184
+ if (callNode.tag === 'structlit' && callNode.fields) {
185
+ fields = callNode.fields;
186
+ }
187
+ // For function calls: client.Track(amplitude.Event{...})
188
+ else if (callNode.args && callNode.args.length > 0) {
189
+ const eventStruct = findStructLiteral(callNode.args[0]);
190
+ if (eventStruct && eventStruct.fields) {
191
+ fields = eventStruct.fields;
192
+ }
193
+ }
194
+
195
+ if (fields) {
196
+ // Extract UserID
197
+ const userIdField = findStructField({ fields }, 'UserID');
198
+ if (userIdField) {
199
+ properties['UserID'] = { type: 'string' };
200
+ }
201
+
202
+ // Extract EventProperties
203
+ const eventPropsField = findStructField({ fields }, 'EventProperties');
204
+ if (eventPropsField) {
205
+ extractPropertiesFromExpr(eventPropsField.value, properties, typeContext, currentFunction);
206
+ }
207
+
208
+ // Extract EventOptions
209
+ const eventOptionsField = findStructField({ fields }, 'EventOptions');
210
+ if (eventOptionsField && eventOptionsField.value) {
211
+ extractEventOptions(eventOptionsField.value, properties, typeContext, currentFunction);
212
+ }
213
+ }
214
+ }
215
+
216
+ /**
217
+ * Extract Snowplow properties
218
+ * Pattern: tracker.TrackStructEvent(sp.StructuredEvent{Category: sphelp.NewString("..."), ...})
219
+ * @param {Object} callNode - AST node for Snowplow tracking call
220
+ * @param {Object} properties - Object to store extracted properties (modified in place)
221
+ * @param {Object} typeContext - Type information context for variable resolution
222
+ * @param {string} currentFunction - Current function context for type lookups
223
+ */
224
+ function extractSnowplowProperties(callNode, properties, typeContext, currentFunction) {
225
+ if (callNode.args && callNode.args.length > 0) {
226
+ const structEvent = findStructLiteral(callNode.args[0]);
227
+ if (structEvent && structEvent.fields) {
228
+ // Extract all fields except Action (which is the event name)
229
+ for (const field of structEvent.fields) {
230
+ const fieldName = extractFieldName(field);
231
+ if (fieldName && fieldName !== 'Action') {
232
+ // Handle both direct values and sphelp.NewString/NewFloat64 calls
233
+ if (field.value) {
234
+ if (field.value.tag === 'expr' && field.value.body) {
235
+ // Look for sphelp.NewString/NewFloat64 calls
236
+ const callNode = field.value.body.find(item =>
237
+ item.tag === 'call' &&
238
+ item.func &&
239
+ item.func.tag === 'access' &&
240
+ (item.func.member === 'NewString' || item.func.member === 'NewFloat64')
241
+ );
242
+
243
+ if (callNode && callNode.args && callNode.args.length > 0) {
244
+ const value = callNode.args[0];
245
+
246
+ // Handle case where value is an expr with the actual value in body[0]
247
+ let actualValue = value;
248
+ if (value.tag === 'expr' && value.body && value.body.length > 0) {
249
+ actualValue = value.body[0];
250
+ }
251
+
252
+ // Use getPropertyInfo to handle all value types including variables
253
+ properties[fieldName] = getPropertyInfo(actualValue, typeContext, currentFunction);
254
+ }
255
+ } else {
256
+ // Handle direct values using getPropertyInfo
257
+ properties[fieldName] = getPropertyInfo(field.value, typeContext, currentFunction);
258
+ }
259
+ }
260
+ }
261
+ }
262
+ }
263
+ }
264
+ }
265
+
266
+ /**
267
+ * Extract custom properties
268
+ * Pattern: customFunction("event_name", map[string]interface{}{...})
269
+ * @param {Object} callNode - AST node for custom tracking function call
270
+ * @param {Object} properties - Object to store extracted properties (modified in place)
271
+ * @param {Object} typeContext - Type information context for variable resolution
272
+ * @param {string} currentFunction - Current function context for type lookups
273
+ */
274
+ function extractCustomProperties(callNode, properties, typeContext, currentFunction) {
275
+ if (callNode.args && callNode.args.length > 1) {
276
+ extractPropertiesFromExpr(callNode.args[1], properties, typeContext, currentFunction);
277
+ }
278
+ }
279
+
280
+ /**
281
+ * Extract properties from chained method calls (e.g., NewProperties().Set())
282
+ * @param {Object} expr - Expression containing chained method calls
283
+ * @param {Object} properties - Object to store extracted properties (modified in place)
284
+ * @param {Object} typeContext - Type information context for variable resolution
285
+ * @param {string} currentFunction - Current function context for type lookups
286
+ */
287
+ function extractChainedProperties(expr, properties, typeContext, currentFunction) {
288
+ if (!expr) return;
289
+
290
+ // Look for method calls in the expression
291
+ if (expr.tag === 'expr' && expr.body) {
292
+ // Find the NewProperties() call in the chain
293
+ const newPropsCall = expr.body.find(item =>
294
+ item.tag === 'access' &&
295
+ item.struct &&
296
+ item.struct.tag === 'call' &&
297
+ item.struct.func &&
298
+ item.struct.func.tag === 'access' &&
299
+ item.struct.func.member === 'NewProperties'
300
+ );
301
+
302
+ if (newPropsCall) {
303
+ // Process all items in the body to find Set() calls
304
+ for (const item of expr.body) {
305
+ // Handle both direct Set() calls and Set() calls in access nodes
306
+ if (item.tag === 'call' && item.func) {
307
+ const funcName = item.func.tag === 'ident' ? item.func.value :
308
+ (item.func.tag === 'access' ? item.func.member : null);
309
+
310
+ if (funcName === 'Set' && item.args && item.args.length >= 2) {
311
+ const key = extractStringValue(item.args[0]);
312
+ if (key) {
313
+ const value = item.args[1];
314
+ // Handle different value types
315
+ if (value.tag === 'expr' && value.body) {
316
+ const firstItem = value.body[0];
317
+ properties[key] = getPropertyInfo(firstItem, typeContext, currentFunction);
318
+ } else {
319
+ properties[key] = getPropertyInfo(value, typeContext, currentFunction);
320
+ }
321
+ }
322
+ }
323
+ } else if (item.tag === 'access' && item.struct && item.struct.tag === 'call') {
324
+ // Handle chained Set() calls
325
+ const call = item.struct;
326
+ if (call.func && call.func.tag === 'ident' && call.func.value === 'Set' && call.args && call.args.length >= 2) {
327
+ const key = extractStringValue(call.args[0]);
328
+ if (key) {
329
+ const value = call.args[1];
330
+ // Handle different value types
331
+ if (value.tag === 'expr' && value.body) {
332
+ const firstItem = value.body[0];
333
+ properties[key] = getPropertyInfo(firstItem, typeContext, currentFunction);
334
+ } else {
335
+ properties[key] = getPropertyInfo(value, typeContext, currentFunction);
336
+ }
337
+ }
338
+ }
339
+ }
340
+ }
341
+ }
342
+ }
343
+ }
344
+
345
+ /**
346
+ * Extract EventOptions from Amplitude events
347
+ * @param {Object} eventOptionsValue - AST node for EventOptions value
348
+ * @param {Object} properties - Object to store extracted properties (modified in place)
349
+ * @param {Object} typeContext - Type information context for variable resolution
350
+ * @param {string} currentFunction - Current function context for type lookups
351
+ */
352
+ function extractEventOptions(eventOptionsValue, properties, typeContext, currentFunction) {
353
+ // Handle the EventOptions struct literal directly
354
+ if (eventOptionsValue.tag === 'structlit' && eventOptionsValue.fields) {
355
+ // Process each field in EventOptions
356
+ for (const field of eventOptionsValue.fields) {
357
+ if (field.name) {
358
+ properties[field.name] = getPropertyInfo(field.value, typeContext, currentFunction);
359
+ }
360
+ }
361
+ return;
362
+ }
363
+
364
+ // Navigate through the expression body to find the structlit (old structure)
365
+ if (eventOptionsValue.tag === 'expr' && eventOptionsValue.body) {
366
+ const exprBody = eventOptionsValue.body;
367
+
368
+ // Look for a structlit in the expression body
369
+ const structlit = exprBody.find(item => item.tag === 'structlit');
370
+ if (structlit && structlit.fields) {
371
+ // Process each field in EventOptions
372
+ for (const field of structlit.fields) {
373
+ if (field.name) {
374
+ properties[field.name] = getPropertyInfo(field.value, typeContext, currentFunction);
375
+ } else if (field.value && field.value.tag === 'expr' && field.value.body) {
376
+ const body = field.value.body;
377
+ if (body.length >= 3 &&
378
+ body[0].tag === 'ident' &&
379
+ body[1].tag === 'op' &&
380
+ body[1].value === ':') {
381
+ const fieldName = body[0].value;
382
+ const value = body[2];
383
+ properties[fieldName] = getPropertyInfo(value, typeContext, currentFunction);
384
+ }
385
+ }
386
+ }
387
+ }
388
+ }
389
+ }
390
+
391
+ /**
392
+ * Extract properties from an expression (handles various forms of property definitions)
393
+ * @param {Object} expr - Expression containing property definitions
394
+ * @param {Object} properties - Object to store extracted properties (modified in place)
395
+ * @param {Object} typeContext - Type information context for variable resolution
396
+ * @param {string} currentFunction - Current function context for type lookups
397
+ */
398
+ function extractPropertiesFromExpr(expr, properties, typeContext, currentFunction) {
399
+ // Handle struct literals (e.g., Type{field: value})
400
+ if (expr.tag === 'structlit' && expr.fields) {
401
+ for (const field of expr.fields) {
402
+ if (field.name) {
403
+ const propInfo = getPropertyInfo(field.value, typeContext, currentFunction);
404
+ properties[field.name] = propInfo;
405
+ } else if (field.value && field.value.tag === 'expr' && field.value.body) {
406
+ // Handle map literal fields that don't have explicit names
407
+ // Format: "key": value
408
+ const keyNode = field.value.body[0];
409
+ const colonNode = field.value.body[1];
410
+ const valueNode = field.value.body[2];
411
+
412
+ if (keyNode && keyNode.tag === 'string' && colonNode && colonNode.value === ':') {
413
+ const key = keyNode.value.slice(1, -1); // Remove quotes
414
+
415
+ // For nested maps, the value might include the map type declaration AND the structlit
416
+ if (valueNode && valueNode.tag === 'index' && valueNode.container && valueNode.container.value === 'map') {
417
+ // Look for the structlit that follows in the body
418
+ const remainingNodes = field.value.body.slice(3); // Skip key, :, and map declaration
419
+ const structlit = remainingNodes.find(node => node.tag === 'structlit');
420
+ if (structlit) {
421
+ properties[key] = getPropertyInfo(structlit, typeContext, currentFunction);
422
+ } else {
423
+ properties[key] = { type: 'object', properties: {} };
424
+ }
425
+ } else if (valueNode) {
426
+ properties[key] = getPropertyInfo(valueNode, typeContext, currentFunction);
427
+ }
428
+ }
429
+ }
430
+ }
431
+ }
432
+
433
+ // Handle expressions that might contain a composite literal
434
+ if (expr.tag === 'expr' && expr.body) {
435
+ for (const item of expr.body) {
436
+ if (item.tag === 'structlit') {
437
+ extractPropertiesFromExpr(item, properties, typeContext, currentFunction);
438
+ } else if (item.tag === 'index' && item.container && item.container.value === 'map') {
439
+ // This is a map[string]interface{} type declaration
440
+ // Look for the following structlit
441
+ continue;
442
+ }
443
+ }
444
+ }
445
+ }
446
+
447
+ /**
448
+ * Get property information from a value node
449
+ * @param {Object} value - AST value node to analyze
450
+ * @param {Object} typeContext - Type information context for variable resolution
451
+ * @param {string} currentFunction - Current function context for type lookups
452
+ * @returns {Object} Type information object with 'type' and optionally 'properties' or 'items'
453
+ */
454
+ function getPropertyInfo(value, typeContext, currentFunction) {
455
+ if (!value) return { type: 'any' };
456
+
457
+ // Handle direct values
458
+ if (value.tag === 'string') {
459
+ return { type: 'string' };
460
+ }
461
+
462
+ if (value.tag === 'number') {
463
+ return { type: 'number' };
464
+ }
465
+
466
+ if (value.tag === 'ident') {
467
+ // Check for boolean constants
468
+ if (value.value === 'true' || value.value === 'false') {
469
+ return { type: 'boolean' };
470
+ }
471
+ if (value.value === 'nil') {
472
+ return { type: 'null' };
473
+ }
474
+ // Look up the variable type in the context
475
+ const varName = value.value;
476
+ let varInfo = null;
477
+
478
+ // Check function parameters first
479
+ if (typeContext && currentFunction && typeContext.functions[currentFunction]) {
480
+ const funcContext = typeContext.functions[currentFunction];
481
+ if (funcContext.params[varName]) {
482
+ varInfo = funcContext.params[varName];
483
+ } else if (funcContext.locals[varName]) {
484
+ varInfo = funcContext.locals[varName];
485
+ }
486
+ }
487
+
488
+ // Check global variables
489
+ if (!varInfo && typeContext && typeContext.globals[varName]) {
490
+ varInfo = typeContext.globals[varName];
491
+ }
492
+
493
+ if (varInfo) {
494
+ // If we have a value stored for this variable, analyze it to get nested properties
495
+ if (varInfo.value && varInfo.type && varInfo.type.tag === 'map') {
496
+ // The variable has a map type and a value, extract properties from the value
497
+ const nestedProps = {};
498
+ extractPropertiesFromExpr(varInfo.value, nestedProps, typeContext, currentFunction);
499
+ return {
500
+ type: 'object',
501
+ properties: nestedProps
502
+ };
503
+ }
504
+ // Otherwise just return the type
505
+ return mapGoTypeToSchemaType(varInfo.type);
506
+ }
507
+
508
+ // Otherwise it's an unknown variable reference
509
+ return { type: 'any' };
510
+ }
511
+
512
+ // Handle index nodes (map[string]interface{})
513
+ if (value.tag === 'index' && value.container && value.container.value === 'map') {
514
+ // This indicates the start of a map literal, look for following structlit
515
+ return { type: 'object', properties: {} };
516
+ }
517
+
518
+ // Handle expressions
519
+ if (value.tag === 'expr' && value.body && value.body.length > 0) {
520
+ const firstItem = value.body[0];
521
+
522
+ // Check for literals
523
+ if (firstItem.tag === 'string') return { type: 'string' };
524
+ if (firstItem.tag === 'number') return { type: 'number' };
525
+ if (firstItem.tag === 'ident') {
526
+ if (firstItem.value === 'true' || firstItem.value === 'false') return { type: 'boolean' };
527
+ if (firstItem.value === 'nil') return { type: 'null' };
528
+ }
529
+
530
+ // Check for array literals
531
+ if (firstItem.tag === 'arraylit') {
532
+ return {
533
+ type: 'array',
534
+ items: { type: 'any' }
535
+ };
536
+ }
537
+
538
+ // Check for map declarations followed by struct literals
539
+ if (firstItem.tag === 'index' && firstItem.container && firstItem.container.value === 'map') {
540
+ // Look for the structlit that follows
541
+ const structlit = value.body.find(item => item.tag === 'structlit');
542
+ if (structlit && structlit.fields) {
543
+ const nestedProps = {};
544
+ // Inline the property extraction for nested objects
545
+ for (const field of structlit.fields) {
546
+ if (field.name) {
547
+ nestedProps[field.name] = getPropertyInfo(field.value, typeContext, currentFunction);
548
+ } else if (field.value && field.value.tag === 'expr' && field.value.body) {
549
+ // Handle map literal fields
550
+ const keyNode = field.value.body[0];
551
+ const colonNode = field.value.body[1];
552
+ const valueNode = field.value.body[2];
553
+
554
+ if (keyNode && keyNode.tag === 'string' && colonNode && colonNode.value === ':') {
555
+ const key = keyNode.value.slice(1, -1);
556
+
557
+ // For nested maps, handle map declaration followed by structlit
558
+ if (valueNode && valueNode.tag === 'index' && valueNode.container && valueNode.container.value === 'map') {
559
+ const remainingNodes = field.value.body.slice(3);
560
+ const structlit = remainingNodes.find(node => node.tag === 'structlit');
561
+ if (structlit) {
562
+ nestedProps[key] = getPropertyInfo(structlit, typeContext, currentFunction);
563
+ } else {
564
+ nestedProps[key] = { type: 'object', properties: {} };
565
+ }
566
+ } else if (valueNode) {
567
+ nestedProps[key] = getPropertyInfo(valueNode, typeContext, currentFunction);
568
+ }
569
+ }
570
+ }
571
+ }
572
+ return {
573
+ type: 'object',
574
+ properties: nestedProps
575
+ };
576
+ }
577
+ }
578
+
579
+ // Check for struct/map literals
580
+ if (firstItem.tag === 'structlit') {
581
+ const nestedProps = {};
582
+ if (firstItem.fields) {
583
+ for (const field of firstItem.fields) {
584
+ if (field.name) {
585
+ nestedProps[field.name] = getPropertyInfo(field.value, typeContext, currentFunction);
586
+ } else if (field.value && field.value.tag === 'expr' && field.value.body) {
587
+ // Handle map literal fields
588
+ const keyNode = field.value.body[0];
589
+ const colonNode = field.value.body[1];
590
+ const valueNode = field.value.body[2];
591
+
592
+ if (keyNode && keyNode.tag === 'string' && colonNode && colonNode.value === ':') {
593
+ const key = keyNode.value.slice(1, -1);
594
+
595
+ // For nested maps, handle map declaration followed by structlit
596
+ if (valueNode && valueNode.tag === 'index' && valueNode.container && valueNode.container.value === 'map') {
597
+ const remainingNodes = field.value.body.slice(3);
598
+ const structlit = remainingNodes.find(node => node.tag === 'structlit');
599
+ if (structlit) {
600
+ nestedProps[key] = getPropertyInfo(structlit, typeContext, currentFunction);
601
+ } else {
602
+ nestedProps[key] = { type: 'object', properties: {} };
603
+ }
604
+ } else if (valueNode) {
605
+ nestedProps[key] = getPropertyInfo(valueNode, typeContext, currentFunction);
606
+ }
607
+ }
608
+ }
609
+ }
610
+ }
611
+ return {
612
+ type: 'object',
613
+ properties: nestedProps
614
+ };
615
+ }
616
+ }
617
+
618
+ // Handle array literals
619
+ if (value.tag === 'arraylit') {
620
+ return {
621
+ type: 'array',
622
+ items: { type: 'any' }
623
+ };
624
+ }
625
+
626
+ // Handle struct literals (nested objects)
627
+ if (value.tag === 'structlit') {
628
+ const nestedProps = {};
629
+ if (value.fields) {
630
+ for (const field of value.fields) {
631
+ if (field.name) {
632
+ nestedProps[field.name] = getPropertyInfo(field.value, typeContext, currentFunction);
633
+ } else if (field.value && field.value.tag === 'expr' && field.value.body) {
634
+ // Handle map literal fields
635
+ const keyNode = field.value.body[0];
636
+ const colonNode = field.value.body[1];
637
+ const valueNode = field.value.body[2];
638
+
639
+ if (keyNode && keyNode.tag === 'string' && colonNode && colonNode.value === ':') {
640
+ const key = keyNode.value.slice(1, -1);
641
+
642
+ // For nested maps, handle map declaration followed by structlit
643
+ if (valueNode && valueNode.tag === 'index' && valueNode.container && valueNode.container.value === 'map') {
644
+ const remainingNodes = field.value.body.slice(3);
645
+ const structlit = remainingNodes.find(node => node.tag === 'structlit');
646
+ if (structlit) {
647
+ nestedProps[key] = getPropertyInfo(structlit, typeContext, currentFunction);
648
+ } else {
649
+ nestedProps[key] = { type: 'object', properties: {} };
650
+ }
651
+ } else if (valueNode) {
652
+ nestedProps[key] = getPropertyInfo(valueNode, typeContext, currentFunction);
653
+ }
654
+ }
655
+ }
656
+ }
657
+ }
658
+ return {
659
+ type: 'object',
660
+ properties: nestedProps
661
+ };
662
+ }
663
+
664
+ // Default to any type
665
+ return { type: 'any' };
666
+ }
667
+
668
+ module.exports = {
669
+ extractProperties
670
+ };