Skip to content

Commit 15eab70

Browse files
committed
multiple custom functions implementation
1 parent dae7859 commit 15eab70

19 files changed

+890
-98
lines changed

src/analyze/go/index.js

Lines changed: 38 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -12,42 +12,59 @@ const { extractEventsFromBody } = require('./astTraversal');
1212
/**
1313
* Analyze a Go file and extract tracking events
1414
* @param {string} filePath - Path to the Go file to analyze
15-
* @param {string|null} customFunctionSignature - Signature of custom tracking function to detect (optional)
15+
* @param {string|null} customFunctionSignatures - Signature of custom tracking function to detect (optional)
1616
* @returns {Promise<Array>} Array of tracking events found in the file
1717
* @throws {Error} If the file cannot be read or parsed
1818
*/
1919
async function analyzeGoFile(filePath, customFunctionSignatures = null) {
2020
try {
21-
// temporary: only support one custom function signature for now, will add support for multiple in the future
22-
const customConfig = !!customFunctionSignatures?.length ? customFunctionSignatures[0] : null;
23-
2421
// Read the Go file
2522
const source = fs.readFileSync(filePath, 'utf8');
26-
27-
// Parse the Go file using goAstParser
23+
24+
// Parse the Go file using goAstParser (once)
2825
const ast = extractGoAST(source);
29-
26+
3027
// First pass: build type information for functions and variables
3128
const typeContext = buildTypeContext(ast);
32-
33-
// Extract tracking events from the AST
34-
const events = [];
35-
let currentFunction = 'global';
36-
37-
// Walk through the AST
38-
for (const node of ast) {
39-
if (node.tag === 'func') {
40-
currentFunction = node.name;
41-
// Process the function body
42-
if (node.body) {
43-
extractEventsFromBody(node.body, events, filePath, currentFunction, customConfig, typeContext, currentFunction);
29+
30+
const collectEventsForConfig = (customConfig) => {
31+
const events = [];
32+
let currentFunction = 'global';
33+
for (const node of ast) {
34+
if (node.tag === 'func') {
35+
currentFunction = node.name;
36+
if (node.body) {
37+
extractEventsFromBody(
38+
node.body,
39+
events,
40+
filePath,
41+
currentFunction,
42+
customConfig,
43+
typeContext,
44+
currentFunction
45+
);
46+
}
4447
}
4548
}
49+
return events;
50+
};
51+
52+
let events = [];
53+
54+
// Built-in providers pass (null custom config)
55+
events.push(...collectEventsForConfig(null));
56+
57+
// Custom configs passes
58+
if (Array.isArray(customFunctionSignatures) && customFunctionSignatures.length > 0) {
59+
for (const customConfig of customFunctionSignatures) {
60+
if (!customConfig) continue;
61+
events.push(...collectEventsForConfig(customConfig));
62+
}
4663
}
47-
64+
4865
// Deduplicate events based on eventName, source, and function
4966
const uniqueEvents = deduplicateEvents(events);
50-
67+
5168
return uniqueEvents;
5269
} catch (error) {
5370
console.error(`Error analyzing Go file ${filePath}:`, error.message);

src/analyze/javascript/extractors/event-extractor.js

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -164,9 +164,18 @@ function processEventData(eventData, source, filePath, line, functionName, custo
164164
// Handle custom extra params
165165
if (source === 'custom' && customConfig && eventData.extraArgs) {
166166
for (const [paramName, argNode] of Object.entries(eventData.extraArgs)) {
167-
properties[paramName] = {
168-
type: inferNodeValueType(argNode)
169-
};
167+
if (argNode && argNode.type === NODE_TYPES.OBJECT_EXPRESSION) {
168+
// Extract detailed properties from object expression
169+
properties[paramName] = {
170+
type: 'object',
171+
properties: extractProperties(argNode)
172+
};
173+
} else {
174+
// For non-object arguments, use simple type inference
175+
properties[paramName] = {
176+
type: inferNodeValueType(argNode)
177+
};
178+
}
170179
}
171180
}
172181

src/analyze/javascript/index.js

Lines changed: 25 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -14,16 +14,33 @@ const { parseFile, findTrackingEvents, FileReadError, ParseError } = require('./
1414
function analyzeJsFile(filePath, customFunctionSignatures = null) {
1515
const events = [];
1616

17-
// temporary: only support one custom function signature for now, will add support for multiple in the future
18-
const customConfig = !!customFunctionSignatures?.length ? customFunctionSignatures[0] : null;
19-
2017
try {
21-
// Parse the file into an AST
18+
// Parse the file into an AST once
2219
const ast = parseFile(filePath);
2320

24-
// Find and extract tracking events
25-
const foundEvents = findTrackingEvents(ast, filePath, customConfig);
26-
events.push(...foundEvents);
21+
// -------- Built-in providers pass --------
22+
const builtInEvents = findTrackingEvents(ast, filePath, null);
23+
events.push(...builtInEvents);
24+
25+
// -------- Custom function passes --------
26+
if (Array.isArray(customFunctionSignatures) && customFunctionSignatures.length > 0) {
27+
for (const customConfig of customFunctionSignatures) {
28+
if (!customConfig) continue;
29+
const customEvents = findTrackingEvents(ast, filePath, customConfig);
30+
events.push(...customEvents);
31+
}
32+
}
33+
34+
// Deduplicate events (by source | eventName | line | functionName)
35+
const uniqueEvents = new Map();
36+
for (const evt of events) {
37+
const key = `${evt.source}|${evt.eventName}|${evt.line}|${evt.functionName}`;
38+
if (!uniqueEvents.has(key)) {
39+
uniqueEvents.set(key, evt);
40+
}
41+
}
42+
43+
return Array.from(uniqueEvents.values());
2744

2845
} catch (error) {
2946
if (error instanceof FileReadError) {
@@ -35,7 +52,7 @@ function analyzeJsFile(filePath, customFunctionSignatures = null) {
3552
}
3653
}
3754

38-
return events;
55+
return [];
3956
}
4057

4158
module.exports = { analyzeJsFile };

src/analyze/python/index.js

Lines changed: 46 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -53,53 +53,69 @@ async function initPyodide() {
5353
* const events = await analyzePythonFile('./app.py', 'track_event');
5454
*/
5555
async function analyzePythonFile(filePath, customFunctionSignatures = null) {
56-
// temporary: only support one custom function signature for now, will add support for multiple in the future
57-
const customConfig = !!customFunctionSignatures?.length ? customFunctionSignatures[0] : null;
58-
5956
// Validate inputs
6057
if (!filePath || typeof filePath !== 'string') {
6158
console.error('Invalid file path provided');
6259
return [];
6360
}
6461

65-
try {
66-
// Check if file exists before reading
67-
if (!fs.existsSync(filePath)) {
68-
console.error(`File not found: ${filePath}`);
69-
return [];
70-
}
62+
// Check if file exists before reading
63+
if (!fs.existsSync(filePath)) {
64+
console.error(`File not found: ${filePath}`);
65+
return [];
66+
}
7167

72-
// Read the Python file
68+
try {
69+
// Read the Python file only once
7370
const code = fs.readFileSync(filePath, 'utf8');
74-
71+
7572
// Initialize Pyodide if not already done
7673
const py = await initPyodide();
77-
78-
// Load the Python analyzer code
74+
75+
// Load the Python analyzer code (idempotent – redefining functions is fine)
7976
const analyzerPath = path.join(__dirname, 'pythonTrackingAnalyzer.py');
8077
if (!fs.existsSync(analyzerPath)) {
8178
throw new Error(`Python analyzer not found at: ${analyzerPath}`);
8279
}
83-
8480
const analyzerCode = fs.readFileSync(analyzerPath, 'utf8');
85-
86-
// Set up Python environment with necessary variables
87-
py.globals.set('code', code);
88-
py.globals.set('filepath', filePath);
89-
py.globals.set('custom_config_json', customConfig ? JSON.stringify(customConfig) : null);
90-
py.runPython('import json');
91-
py.runPython('custom_config = None if custom_config_json == None else json.loads(custom_config_json)');
92-
// Set __name__ to null to prevent execution of main block
81+
// Prevent the analyzer from executing any __main__ blocks that expect CLI usage
9382
py.globals.set('__name__', null);
94-
95-
// Load and run the analyzer
9683
py.runPython(analyzerCode);
97-
98-
// Execute the analysis and parse result
99-
const result = py.runPython('analyze_python_code(code, filepath, custom_config)');
100-
const events = JSON.parse(result);
101-
102-
return events;
84+
85+
// Helper to run analysis with a given custom config (can be null)
86+
const runAnalysis = (customConfig) => {
87+
py.globals.set('code', code);
88+
py.globals.set('filepath', filePath);
89+
py.globals.set('custom_config_json', customConfig ? JSON.stringify(customConfig) : null);
90+
py.runPython('import json');
91+
py.runPython('custom_config = None if custom_config_json == None else json.loads(custom_config_json)');
92+
const result = py.runPython('analyze_python_code(code, filepath, custom_config)');
93+
return JSON.parse(result);
94+
};
95+
96+
const events = [];
97+
98+
// Built-in providers pass (no custom config)
99+
events.push(...runAnalysis(null));
100+
101+
// Custom configs passes
102+
if (Array.isArray(customFunctionSignatures) && customFunctionSignatures.length > 0) {
103+
for (const customConfig of customFunctionSignatures) {
104+
if (!customConfig) continue;
105+
events.push(...runAnalysis(customConfig));
106+
}
107+
}
108+
109+
// Deduplicate events
110+
const uniqueEvents = new Map();
111+
for (const evt of events) {
112+
const key = `${evt.source}|${evt.eventName}|${evt.line}|${evt.functionName}`;
113+
if (!uniqueEvents.has(key)) {
114+
uniqueEvents.set(key, evt);
115+
}
116+
}
117+
118+
return Array.from(uniqueEvents.values());
103119
} catch (error) {
104120
// Log detailed error information for debugging
105121
console.error(`Error analyzing Python file ${filePath}:`, error);

src/analyze/ruby/index.js

Lines changed: 28 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -26,24 +26,43 @@ async function analyzeRubyFile(filePath, customFunctionSignatures = null) {
2626
try {
2727
// Read the file content
2828
const code = fs.readFileSync(filePath, 'utf8');
29-
30-
// Parse the Ruby code into an AST
29+
30+
// Parse the Ruby code into an AST once
3131
let ast;
3232
try {
3333
ast = await parse(code);
3434
} catch (parseError) {
3535
console.error(`Error parsing file ${filePath}:`, parseError.message);
36-
return []; // Return empty events array if parsing fails
36+
return [];
3737
}
3838

39-
// temporary: only support one custom function signature for now, will add support for multiple in the future
40-
const customConfig = !!customFunctionSignatures?.length ? customFunctionSignatures[0] : null;
39+
const events = [];
40+
41+
// -------- Built-in providers pass --------
42+
let visitor = new TrackingVisitor(code, filePath, null);
43+
const builtInEvents = await visitor.analyze(ast);
44+
events.push(...builtInEvents);
4145

42-
// Create a visitor and analyze the AST
43-
const visitor = new TrackingVisitor(code, filePath, customConfig);
44-
const events = await visitor.analyze(ast);
46+
// -------- Custom config passes --------
47+
if (Array.isArray(customFunctionSignatures) && customFunctionSignatures.length > 0) {
48+
for (const customConfig of customFunctionSignatures) {
49+
if (!customConfig) continue;
50+
const customVisitor = new TrackingVisitor(code, filePath, customConfig);
51+
const customEvents = await customVisitor.analyze(ast);
52+
events.push(...customEvents);
53+
}
54+
}
55+
56+
// Deduplicate events
57+
const uniqueEvents = new Map();
58+
for (const evt of events) {
59+
const key = `${evt.source}|${evt.eventName}|${evt.line}|${evt.functionName}`;
60+
if (!uniqueEvents.has(key)) {
61+
uniqueEvents.set(key, evt);
62+
}
63+
}
4564

46-
return events;
65+
return Array.from(uniqueEvents.values());
4766

4867
} catch (fileError) {
4968
console.error(`Error reading or processing file ${filePath}:`, fileError.message);

src/analyze/typescript/extractors/event-extractor.js

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -219,9 +219,31 @@ function processEventData(eventData, source, filePath, line, functionName, check
219219
// Handle custom extra params
220220
if (source === 'custom' && customConfig && eventData.extraArgs) {
221221
for (const [paramName, argNode] of Object.entries(eventData.extraArgs)) {
222-
cleanedProperties[paramName] = {
223-
type: inferNodeValueType(argNode)
224-
};
222+
if (argNode && ts.isObjectLiteralExpression(argNode)) {
223+
// Extract detailed properties from object literal expression
224+
cleanedProperties[paramName] = {
225+
type: 'object',
226+
properties: extractProperties(checker, argNode)
227+
};
228+
} else if (argNode && ts.isIdentifier(argNode)) {
229+
// Handle identifier references to objects
230+
const resolvedNode = resolveIdentifierToInitializer(checker, argNode, sourceFile);
231+
if (resolvedNode && ts.isObjectLiteralExpression(resolvedNode)) {
232+
cleanedProperties[paramName] = {
233+
type: 'object',
234+
properties: extractProperties(checker, resolvedNode)
235+
};
236+
} else {
237+
cleanedProperties[paramName] = {
238+
type: inferNodeValueType(argNode)
239+
};
240+
}
241+
} else {
242+
// For non-object arguments, use simple type inference
243+
cleanedProperties[paramName] = {
244+
type: inferNodeValueType(argNode)
245+
};
246+
}
225247
}
226248
}
227249

src/analyze/typescript/index.js

Lines changed: 26 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,10 @@ const { getProgram, findTrackingEvents, ProgramError, SourceFileError } = requir
1515
function analyzeTsFile(filePath, program = null, customFunctionSignatures = null) {
1616
const events = [];
1717

18-
// temporary: only support one custom function signature for now, will add support for multiple in the future
19-
const customConfig = !!customFunctionSignatures?.length ? customFunctionSignatures[0] : null;
20-
2118
try {
22-
// Get or create TypeScript program
19+
// Get or create TypeScript program (only once)
2320
const tsProgram = getProgram(filePath, program);
24-
21+
2522
// Get source file from program
2623
const sourceFile = tsProgram.getSourceFile(filePath);
2724
if (!sourceFile) {
@@ -31,9 +28,29 @@ function analyzeTsFile(filePath, program = null, customFunctionSignatures = null
3128
// Get type checker
3229
const checker = tsProgram.getTypeChecker();
3330

34-
// Find and extract tracking events
35-
const foundEvents = findTrackingEvents(sourceFile, checker, filePath, customConfig);
36-
events.push(...foundEvents);
31+
// -------- Built-in providers pass --------
32+
const builtInEvents = findTrackingEvents(sourceFile, checker, filePath, null);
33+
events.push(...builtInEvents);
34+
35+
// -------- Custom function passes --------
36+
if (Array.isArray(customFunctionSignatures) && customFunctionSignatures.length > 0) {
37+
for (const customConfig of customFunctionSignatures) {
38+
if (!customConfig) continue;
39+
const customEvents = findTrackingEvents(sourceFile, checker, filePath, customConfig);
40+
events.push(...customEvents);
41+
}
42+
}
43+
44+
// Deduplicate events (source|eventName|line|functionName)
45+
const uniqueEvents = new Map();
46+
for (const evt of events) {
47+
const key = `${evt.source}|${evt.eventName}|${evt.line}|${evt.functionName}`;
48+
if (!uniqueEvents.has(key)) {
49+
uniqueEvents.set(key, evt);
50+
}
51+
}
52+
53+
return Array.from(uniqueEvents.values());
3754

3855
} catch (error) {
3956
if (error instanceof ProgramError) {
@@ -45,7 +62,7 @@ function analyzeTsFile(filePath, program = null, customFunctionSignatures = null
4562
}
4663
}
4764

48-
return events;
65+
return [];
4966
}
5067

5168
module.exports = { analyzeTsFile };

0 commit comments

Comments
 (0)