@flisk/analyze-tracking 0.7.2 → 0.7.4
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/README.md +4 -0
- package/bin/cli.js +30 -2
- package/package.json +12 -8
- package/src/analyze/go/astTraversal.js +121 -0
- package/src/analyze/go/constants.js +20 -0
- package/src/analyze/go/eventDeduplicator.js +47 -0
- package/src/analyze/go/eventExtractor.js +156 -0
- package/src/analyze/go/goAstParser/constants.js +39 -0
- package/src/analyze/go/goAstParser/expressionParser.js +281 -0
- package/src/analyze/go/goAstParser/index.js +52 -0
- package/src/analyze/go/goAstParser/statementParser.js +387 -0
- package/src/analyze/go/goAstParser/tokenizer.js +196 -0
- package/src/analyze/go/goAstParser/typeParser.js +202 -0
- package/src/analyze/go/goAstParser/utils.js +99 -0
- package/src/analyze/go/index.js +55 -0
- package/src/analyze/go/propertyExtractor.js +670 -0
- package/src/analyze/go/trackingDetector.js +71 -0
- package/src/analyze/go/trackingExtractor.js +54 -0
- package/src/analyze/go/typeContext.js +88 -0
- package/src/analyze/go/utils.js +215 -0
- package/src/analyze/index.js +11 -6
- package/src/analyze/javascript/constants.js +115 -0
- package/src/analyze/javascript/detectors/analytics-source.js +119 -0
- package/src/analyze/javascript/detectors/index.js +10 -0
- package/src/analyze/javascript/extractors/event-extractor.js +179 -0
- package/src/analyze/javascript/extractors/index.js +13 -0
- package/src/analyze/javascript/extractors/property-extractor.js +172 -0
- package/src/analyze/javascript/index.js +38 -0
- package/src/analyze/javascript/parser.js +126 -0
- package/src/analyze/javascript/utils/function-finder.js +123 -0
- package/src/analyze/python/index.js +111 -0
- package/src/analyze/python/pythonTrackingAnalyzer.py +814 -0
- package/src/analyze/ruby/detectors.js +46 -0
- package/src/analyze/ruby/extractors.js +258 -0
- package/src/analyze/ruby/index.js +51 -0
- package/src/analyze/ruby/traversal.js +123 -0
- package/src/analyze/ruby/types.js +30 -0
- package/src/analyze/ruby/visitor.js +66 -0
- package/src/analyze/typescript/constants.js +109 -0
- package/src/analyze/typescript/detectors/analytics-source.js +125 -0
- package/src/analyze/typescript/detectors/index.js +10 -0
- package/src/analyze/typescript/extractors/event-extractor.js +269 -0
- package/src/analyze/typescript/extractors/index.js +14 -0
- package/src/analyze/typescript/extractors/property-extractor.js +427 -0
- package/src/analyze/typescript/index.js +48 -0
- package/src/analyze/typescript/parser.js +131 -0
- package/src/analyze/typescript/utils/function-finder.js +139 -0
- package/src/analyze/typescript/utils/type-resolver.js +208 -0
- package/src/generateDescriptions/index.js +81 -0
- package/src/generateDescriptions/llmUtils.js +33 -0
- package/src/generateDescriptions/promptUtils.js +62 -0
- package/src/generateDescriptions/schemaUtils.js +61 -0
- package/src/index.js +13 -4
- package/src/{fileProcessor.js → utils/fileProcessor.js} +5 -0
- package/src/{repoDetails.js → utils/repoDetails.js} +5 -0
- package/src/utils/yamlGenerator.js +47 -0
- package/src/analyze/analyzeGoFile.js +0 -1164
- package/src/analyze/analyzeJsFile.js +0 -87
- package/src/analyze/analyzePythonFile.js +0 -42
- package/src/analyze/analyzeRubyFile.js +0 -419
- package/src/analyze/analyzeTsFile.js +0 -192
- package/src/analyze/go2json.js +0 -1069
- package/src/analyze/helpers.js +0 -656
- package/src/analyze/pythonTrackingAnalyzer.py +0 -541
- package/src/generateDescriptions.js +0 -196
- package/src/yamlGenerator.js +0 -23
|
@@ -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
|
+
};
|