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