@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
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Event extraction logic for different analytics providers
|
|
3
|
+
* @module analyze/javascript/extractors/event-extractor
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const { NODE_TYPES } = require('../constants');
|
|
7
|
+
const { extractProperties } = require('./property-extractor');
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Event data structure
|
|
11
|
+
* @typedef {Object} EventData
|
|
12
|
+
* @property {string|null} eventName - The event name
|
|
13
|
+
* @property {Object|null} propertiesNode - AST node containing event properties
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Provider-specific extraction strategies
|
|
18
|
+
*/
|
|
19
|
+
const EXTRACTION_STRATEGIES = {
|
|
20
|
+
googleanalytics: extractGoogleAnalyticsEvent,
|
|
21
|
+
snowplow: extractSnowplowEvent,
|
|
22
|
+
mparticle: extractMparticleEvent,
|
|
23
|
+
default: extractDefaultEvent
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Extracts event information from a CallExpression node
|
|
28
|
+
* @param {Object} node - AST CallExpression node
|
|
29
|
+
* @param {string} source - Analytics provider source
|
|
30
|
+
* @returns {EventData} Extracted event data
|
|
31
|
+
*/
|
|
32
|
+
function extractEventData(node, source) {
|
|
33
|
+
const strategy = EXTRACTION_STRATEGIES[source] || EXTRACTION_STRATEGIES.default;
|
|
34
|
+
return strategy(node);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Extracts Google Analytics event data
|
|
39
|
+
* @param {Object} node - CallExpression node
|
|
40
|
+
* @returns {EventData}
|
|
41
|
+
*/
|
|
42
|
+
function extractGoogleAnalyticsEvent(node) {
|
|
43
|
+
if (!node.arguments || node.arguments.length < 3) {
|
|
44
|
+
return { eventName: null, propertiesNode: null };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// gtag('event', 'event_name', { properties })
|
|
48
|
+
const eventName = getStringValue(node.arguments[1]);
|
|
49
|
+
const propertiesNode = node.arguments[2];
|
|
50
|
+
|
|
51
|
+
return { eventName, propertiesNode };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Extracts Snowplow event data
|
|
56
|
+
* @param {Object} node - CallExpression node
|
|
57
|
+
* @returns {EventData}
|
|
58
|
+
*/
|
|
59
|
+
function extractSnowplowEvent(node) {
|
|
60
|
+
if (!node.arguments || node.arguments.length === 0) {
|
|
61
|
+
return { eventName: null, propertiesNode: null };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// tracker.track(buildStructEvent({ action: 'event_name', ... }))
|
|
65
|
+
const firstArg = node.arguments[0];
|
|
66
|
+
|
|
67
|
+
if (firstArg.type === NODE_TYPES.CALL_EXPRESSION &&
|
|
68
|
+
firstArg.arguments.length > 0) {
|
|
69
|
+
const structEventArg = firstArg.arguments[0];
|
|
70
|
+
|
|
71
|
+
if (structEventArg.type === NODE_TYPES.OBJECT_EXPRESSION) {
|
|
72
|
+
const actionProperty = findPropertyByKey(structEventArg, 'action');
|
|
73
|
+
const eventName = actionProperty ? getStringValue(actionProperty.value) : null;
|
|
74
|
+
|
|
75
|
+
return { eventName, propertiesNode: structEventArg };
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return { eventName: null, propertiesNode: null };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Extracts mParticle event data
|
|
84
|
+
* @param {Object} node - CallExpression node
|
|
85
|
+
* @returns {EventData}
|
|
86
|
+
*/
|
|
87
|
+
function extractMparticleEvent(node) {
|
|
88
|
+
if (!node.arguments || node.arguments.length < 3) {
|
|
89
|
+
return { eventName: null, propertiesNode: null };
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// mParticle.logEvent('event_name', mParticle.EventType.Navigation, { properties })
|
|
93
|
+
const eventName = getStringValue(node.arguments[0]);
|
|
94
|
+
const propertiesNode = node.arguments[2];
|
|
95
|
+
|
|
96
|
+
return { eventName, propertiesNode };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Default event extraction for standard providers
|
|
101
|
+
* @param {Object} node - CallExpression node
|
|
102
|
+
* @returns {EventData}
|
|
103
|
+
*/
|
|
104
|
+
function extractDefaultEvent(node) {
|
|
105
|
+
if (!node.arguments || node.arguments.length < 2) {
|
|
106
|
+
return { eventName: null, propertiesNode: null };
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// provider.track('event_name', { properties })
|
|
110
|
+
const eventName = getStringValue(node.arguments[0]);
|
|
111
|
+
const propertiesNode = node.arguments[1];
|
|
112
|
+
|
|
113
|
+
return { eventName, propertiesNode };
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Processes extracted event data into final event object
|
|
118
|
+
* @param {EventData} eventData - Raw event data
|
|
119
|
+
* @param {string} source - Analytics source
|
|
120
|
+
* @param {string} filePath - File path
|
|
121
|
+
* @param {number} line - Line number
|
|
122
|
+
* @param {string} functionName - Containing function name
|
|
123
|
+
* @returns {Object|null} Processed event object or null
|
|
124
|
+
*/
|
|
125
|
+
function processEventData(eventData, source, filePath, line, functionName) {
|
|
126
|
+
const { eventName, propertiesNode } = eventData;
|
|
127
|
+
|
|
128
|
+
if (!eventName || !propertiesNode || propertiesNode.type !== NODE_TYPES.OBJECT_EXPRESSION) {
|
|
129
|
+
return null;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
let properties = extractProperties(propertiesNode);
|
|
133
|
+
|
|
134
|
+
// Special handling for Snowplow: remove 'action' from properties
|
|
135
|
+
if (source === 'snowplow' && properties.action) {
|
|
136
|
+
delete properties.action;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return {
|
|
140
|
+
eventName,
|
|
141
|
+
source,
|
|
142
|
+
properties,
|
|
143
|
+
filePath,
|
|
144
|
+
line,
|
|
145
|
+
functionName
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Gets string value from an AST node
|
|
151
|
+
* @param {Object} node - AST node
|
|
152
|
+
* @returns {string|null} String value or null
|
|
153
|
+
*/
|
|
154
|
+
function getStringValue(node) {
|
|
155
|
+
if (!node) return null;
|
|
156
|
+
if (node.type === NODE_TYPES.LITERAL && typeof node.value === 'string') {
|
|
157
|
+
return node.value;
|
|
158
|
+
}
|
|
159
|
+
return null;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Finds a property by key in an ObjectExpression
|
|
164
|
+
* @param {Object} objectNode - ObjectExpression node
|
|
165
|
+
* @param {string} key - Property key to find
|
|
166
|
+
* @returns {Object|null} Property node or null
|
|
167
|
+
*/
|
|
168
|
+
function findPropertyByKey(objectNode, key) {
|
|
169
|
+
if (!objectNode.properties) return null;
|
|
170
|
+
|
|
171
|
+
return objectNode.properties.find(prop =>
|
|
172
|
+
prop.key && (prop.key.name === key || prop.key.value === key)
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
module.exports = {
|
|
177
|
+
extractEventData,
|
|
178
|
+
processEventData
|
|
179
|
+
};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Central export for all extractor modules
|
|
3
|
+
* @module analyze/javascript/extractors
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const { extractEventData, processEventData } = require('./event-extractor');
|
|
7
|
+
const { extractProperties } = require('./property-extractor');
|
|
8
|
+
|
|
9
|
+
module.exports = {
|
|
10
|
+
extractEventData,
|
|
11
|
+
processEventData,
|
|
12
|
+
extractProperties
|
|
13
|
+
};
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Property extraction from AST nodes
|
|
3
|
+
* @module analyze/javascript/extractors/property-extractor
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const { NODE_TYPES } = require('../constants');
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Property structure representation
|
|
10
|
+
* @typedef {Object} PropertySchema
|
|
11
|
+
* @property {string} type - The property type (string, number, boolean, object, array, any)
|
|
12
|
+
* @property {PropertySchema} [properties] - Nested properties for objects
|
|
13
|
+
* @property {Object} [items] - Item type information for arrays
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Extracts properties from an ObjectExpression node
|
|
18
|
+
* @param {Object} node - AST ObjectExpression node
|
|
19
|
+
* @returns {Object.<string, PropertySchema>} Extracted properties with their schemas
|
|
20
|
+
*/
|
|
21
|
+
function extractProperties(node) {
|
|
22
|
+
if (!node || node.type !== NODE_TYPES.OBJECT_EXPRESSION) {
|
|
23
|
+
return {};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const properties = {};
|
|
27
|
+
|
|
28
|
+
node.properties.forEach((prop) => {
|
|
29
|
+
const key = getPropertyKey(prop);
|
|
30
|
+
if (!key) return;
|
|
31
|
+
|
|
32
|
+
const schema = extractPropertySchema(prop.value);
|
|
33
|
+
if (schema) {
|
|
34
|
+
properties[key] = schema;
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
return properties;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Gets the key name from a property node
|
|
43
|
+
* @param {Object} prop - Property node
|
|
44
|
+
* @returns {string|null} Property key or null
|
|
45
|
+
*/
|
|
46
|
+
function getPropertyKey(prop) {
|
|
47
|
+
if (!prop.key) return null;
|
|
48
|
+
return prop.key.name || prop.key.value || null;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Extracts schema information from a property value
|
|
53
|
+
* @param {Object} valueNode - AST node representing the property value
|
|
54
|
+
* @returns {PropertySchema|null} Property schema or null
|
|
55
|
+
*/
|
|
56
|
+
function extractPropertySchema(valueNode) {
|
|
57
|
+
if (!valueNode) return null;
|
|
58
|
+
|
|
59
|
+
switch (valueNode.type) {
|
|
60
|
+
case NODE_TYPES.OBJECT_EXPRESSION:
|
|
61
|
+
return extractObjectSchema(valueNode);
|
|
62
|
+
|
|
63
|
+
case NODE_TYPES.ARRAY_EXPRESSION:
|
|
64
|
+
return extractArraySchema(valueNode);
|
|
65
|
+
|
|
66
|
+
case NODE_TYPES.LITERAL:
|
|
67
|
+
return extractLiteralSchema(valueNode);
|
|
68
|
+
|
|
69
|
+
default:
|
|
70
|
+
return { type: 'any' };
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Extracts schema for object expressions
|
|
76
|
+
* @param {Object} node - ObjectExpression node
|
|
77
|
+
* @returns {PropertySchema}
|
|
78
|
+
*/
|
|
79
|
+
function extractObjectSchema(node) {
|
|
80
|
+
return {
|
|
81
|
+
type: 'object',
|
|
82
|
+
properties: extractProperties(node)
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Extracts schema for array expressions
|
|
88
|
+
* @param {Object} node - ArrayExpression node
|
|
89
|
+
* @returns {PropertySchema}
|
|
90
|
+
*/
|
|
91
|
+
function extractArraySchema(node) {
|
|
92
|
+
const itemType = inferArrayItemType(node.elements);
|
|
93
|
+
|
|
94
|
+
return {
|
|
95
|
+
type: 'array',
|
|
96
|
+
items: {
|
|
97
|
+
type: itemType
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Extracts schema for literal values
|
|
104
|
+
* @param {Object} node - Literal node
|
|
105
|
+
* @returns {PropertySchema}
|
|
106
|
+
*/
|
|
107
|
+
function extractLiteralSchema(node) {
|
|
108
|
+
const valueType = typeof node.value;
|
|
109
|
+
|
|
110
|
+
// Handle null and undefined
|
|
111
|
+
if (node.value === null || valueType === 'undefined') {
|
|
112
|
+
return { type: 'any' };
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Handle valid primitive types
|
|
116
|
+
if (['string', 'number', 'boolean'].includes(valueType)) {
|
|
117
|
+
return { type: valueType };
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Default to 'any' for other types
|
|
121
|
+
return { type: 'any' };
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Infers the item type of an array from its elements
|
|
126
|
+
* @param {Array} elements - Array of AST nodes
|
|
127
|
+
* @returns {string} Inferred type or 'any'
|
|
128
|
+
*/
|
|
129
|
+
function inferArrayItemType(elements) {
|
|
130
|
+
if (!elements || elements.length === 0) {
|
|
131
|
+
return 'any';
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const types = new Set();
|
|
135
|
+
|
|
136
|
+
elements.forEach(element => {
|
|
137
|
+
if (!element) return;
|
|
138
|
+
|
|
139
|
+
const elementType = getElementType(element);
|
|
140
|
+
types.add(elementType);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
// If all elements have the same type, use that type
|
|
144
|
+
if (types.size === 1) {
|
|
145
|
+
return Array.from(types)[0];
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Mixed types default to 'any'
|
|
149
|
+
return 'any';
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Gets the type of an array element
|
|
154
|
+
* @param {Object} element - AST node
|
|
155
|
+
* @returns {string} Element type
|
|
156
|
+
*/
|
|
157
|
+
function getElementType(element) {
|
|
158
|
+
switch (element.type) {
|
|
159
|
+
case NODE_TYPES.LITERAL:
|
|
160
|
+
return typeof element.value;
|
|
161
|
+
case NODE_TYPES.OBJECT_EXPRESSION:
|
|
162
|
+
return 'object';
|
|
163
|
+
case NODE_TYPES.ARRAY_EXPRESSION:
|
|
164
|
+
return 'array';
|
|
165
|
+
default:
|
|
166
|
+
return 'any';
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
module.exports = {
|
|
171
|
+
extractProperties
|
|
172
|
+
};
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview JavaScript analytics tracking analyzer - main entry point
|
|
3
|
+
* @module analyze/javascript
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const { parseFile, findTrackingEvents, FileReadError, ParseError } = require('./parser');
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Analyzes a JavaScript file for analytics tracking calls
|
|
10
|
+
* @param {string} filePath - Path to the JavaScript file to analyze
|
|
11
|
+
* @param {string} [customFunction] - Optional custom function name to detect
|
|
12
|
+
* @returns {Array<Object>} Array of tracking events found in the file
|
|
13
|
+
*/
|
|
14
|
+
function analyzeJsFile(filePath, customFunction) {
|
|
15
|
+
const events = [];
|
|
16
|
+
|
|
17
|
+
try {
|
|
18
|
+
// Parse the file into an AST
|
|
19
|
+
const ast = parseFile(filePath);
|
|
20
|
+
|
|
21
|
+
// Find and extract tracking events
|
|
22
|
+
const foundEvents = findTrackingEvents(ast, filePath, customFunction);
|
|
23
|
+
events.push(...foundEvents);
|
|
24
|
+
|
|
25
|
+
} catch (error) {
|
|
26
|
+
if (error instanceof FileReadError) {
|
|
27
|
+
console.error(`Error reading file ${filePath}: ${error.originalError.message}`);
|
|
28
|
+
} else if (error instanceof ParseError) {
|
|
29
|
+
console.error(`Error parsing file ${filePath}: ${error.originalError.message}`);
|
|
30
|
+
} else {
|
|
31
|
+
console.error(`Unexpected error analyzing ${filePath}: ${error.message}`);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return events;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
module.exports = { analyzeJsFile };
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview AST parsing and walking module
|
|
3
|
+
* @module analyze/javascript/parser
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const fs = require('fs');
|
|
7
|
+
const acorn = require('acorn');
|
|
8
|
+
const jsx = require('acorn-jsx');
|
|
9
|
+
const walk = require('acorn-walk');
|
|
10
|
+
const { extend } = require('acorn-jsx-walk');
|
|
11
|
+
const { PARSER_OPTIONS, NODE_TYPES } = require('./constants');
|
|
12
|
+
const { detectAnalyticsSource } = require('./detectors');
|
|
13
|
+
const { extractEventData, processEventData } = require('./extractors');
|
|
14
|
+
const { findWrappingFunction } = require('./utils/function-finder');
|
|
15
|
+
|
|
16
|
+
// Extend walker to support JSX
|
|
17
|
+
extend(walk.base);
|
|
18
|
+
|
|
19
|
+
// Configure parser with JSX support
|
|
20
|
+
const parser = acorn.Parser.extend(jsx());
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Error thrown when file cannot be read
|
|
24
|
+
*/
|
|
25
|
+
class FileReadError extends Error {
|
|
26
|
+
constructor(filePath, originalError) {
|
|
27
|
+
super(`Failed to read file: ${filePath}`);
|
|
28
|
+
this.name = 'FileReadError';
|
|
29
|
+
this.filePath = filePath;
|
|
30
|
+
this.originalError = originalError;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Error thrown when file cannot be parsed
|
|
36
|
+
*/
|
|
37
|
+
class ParseError extends Error {
|
|
38
|
+
constructor(filePath, originalError) {
|
|
39
|
+
super(`Failed to parse file: ${filePath}`);
|
|
40
|
+
this.name = 'ParseError';
|
|
41
|
+
this.filePath = filePath;
|
|
42
|
+
this.originalError = originalError;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Parses a JavaScript file and returns its AST
|
|
48
|
+
* @param {string} filePath - Path to the JavaScript file
|
|
49
|
+
* @returns {Object} Parsed AST
|
|
50
|
+
* @throws {FileReadError} If file cannot be read
|
|
51
|
+
* @throws {ParseError} If file cannot be parsed
|
|
52
|
+
*/
|
|
53
|
+
function parseFile(filePath) {
|
|
54
|
+
let code;
|
|
55
|
+
|
|
56
|
+
try {
|
|
57
|
+
code = fs.readFileSync(filePath, 'utf8');
|
|
58
|
+
} catch (error) {
|
|
59
|
+
throw new FileReadError(filePath, error);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
try {
|
|
63
|
+
return parser.parse(code, PARSER_OPTIONS);
|
|
64
|
+
} catch (error) {
|
|
65
|
+
throw new ParseError(filePath, error);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Walks the AST and finds analytics tracking calls
|
|
71
|
+
* @param {Object} ast - Parsed AST
|
|
72
|
+
* @param {string} filePath - Path to the file being analyzed
|
|
73
|
+
* @param {string} [customFunction] - Custom function name to detect
|
|
74
|
+
* @returns {Array<Object>} Array of found events
|
|
75
|
+
*/
|
|
76
|
+
function findTrackingEvents(ast, filePath, customFunction) {
|
|
77
|
+
const events = [];
|
|
78
|
+
|
|
79
|
+
walk.ancestor(ast, {
|
|
80
|
+
[NODE_TYPES.CALL_EXPRESSION]: (node, ancestors) => {
|
|
81
|
+
try {
|
|
82
|
+
const event = extractTrackingEvent(node, ancestors, filePath, customFunction);
|
|
83
|
+
if (event) {
|
|
84
|
+
events.push(event);
|
|
85
|
+
}
|
|
86
|
+
} catch (error) {
|
|
87
|
+
console.error(`Error processing node in ${filePath}:`, error.message);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
return events;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Extracts tracking event from a CallExpression node
|
|
97
|
+
* @param {Object} node - CallExpression node
|
|
98
|
+
* @param {Array<Object>} ancestors - Ancestor nodes
|
|
99
|
+
* @param {string} filePath - File path
|
|
100
|
+
* @param {string} [customFunction] - Custom function name
|
|
101
|
+
* @returns {Object|null} Extracted event or null
|
|
102
|
+
*/
|
|
103
|
+
function extractTrackingEvent(node, ancestors, filePath, customFunction) {
|
|
104
|
+
// Detect the analytics source
|
|
105
|
+
const source = detectAnalyticsSource(node, customFunction);
|
|
106
|
+
if (source === 'unknown') {
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Extract event data based on the source
|
|
111
|
+
const eventData = extractEventData(node, source);
|
|
112
|
+
|
|
113
|
+
// Get location and context information
|
|
114
|
+
const line = node.loc.start.line;
|
|
115
|
+
const functionName = findWrappingFunction(node, ancestors);
|
|
116
|
+
|
|
117
|
+
// Process the event data into final format
|
|
118
|
+
return processEventData(eventData, source, filePath, line, functionName);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
module.exports = {
|
|
122
|
+
parseFile,
|
|
123
|
+
findTrackingEvents,
|
|
124
|
+
FileReadError,
|
|
125
|
+
ParseError
|
|
126
|
+
};
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Utilities for finding function context in AST
|
|
3
|
+
* @module analyze/javascript/utils/function-finder
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const { NODE_TYPES } = require('../constants');
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Finds the name of the function that wraps a given node
|
|
10
|
+
* @param {Object} node - The AST node to find the wrapper for
|
|
11
|
+
* @param {Array<Object>} ancestors - Array of ancestor nodes from acorn-walk
|
|
12
|
+
* @returns {string} The function name or 'global' if not in a function
|
|
13
|
+
*/
|
|
14
|
+
function findWrappingFunction(node, ancestors) {
|
|
15
|
+
// Traverse ancestors from closest to furthest
|
|
16
|
+
for (let i = ancestors.length - 1; i >= 0; i--) {
|
|
17
|
+
const current = ancestors[i];
|
|
18
|
+
const functionName = extractFunctionName(current, node, ancestors[i - 1]);
|
|
19
|
+
|
|
20
|
+
if (functionName) {
|
|
21
|
+
return functionName;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return 'global';
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Extracts function name from different AST node types
|
|
30
|
+
* @param {Object} current - Current ancestor node
|
|
31
|
+
* @param {Object} node - Original node being analyzed
|
|
32
|
+
* @param {Object} [parent] - Parent of current node
|
|
33
|
+
* @returns {string|null} Function name or null if not a function context
|
|
34
|
+
*/
|
|
35
|
+
function extractFunctionName(current, node, parent) {
|
|
36
|
+
switch (current.type) {
|
|
37
|
+
case NODE_TYPES.VARIABLE_DECLARATOR:
|
|
38
|
+
return handleVariableDeclarator(current, node);
|
|
39
|
+
|
|
40
|
+
case NODE_TYPES.FUNCTION_DECLARATION:
|
|
41
|
+
return current.id ? current.id.name : 'anonymous';
|
|
42
|
+
|
|
43
|
+
case NODE_TYPES.METHOD_DEFINITION:
|
|
44
|
+
return current.key.name || 'anonymous';
|
|
45
|
+
|
|
46
|
+
case NODE_TYPES.PROPERTY:
|
|
47
|
+
return handleObjectProperty(current, node);
|
|
48
|
+
|
|
49
|
+
case NODE_TYPES.EXPORT_NAMED:
|
|
50
|
+
return handleNamedExport(current);
|
|
51
|
+
|
|
52
|
+
default:
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Handles variable declarator nodes (const/let/var declarations)
|
|
59
|
+
* @param {Object} declarator - VariableDeclarator node
|
|
60
|
+
* @param {Object} node - Original node being analyzed
|
|
61
|
+
* @returns {string|null} Function name or null
|
|
62
|
+
*/
|
|
63
|
+
function handleVariableDeclarator(declarator, node) {
|
|
64
|
+
// Direct assignment: const myFunc = () => {}
|
|
65
|
+
if (declarator.init === node) {
|
|
66
|
+
return declarator.id.name;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Function expression assignment
|
|
70
|
+
if (declarator.init && isFunctionNode(declarator.init)) {
|
|
71
|
+
return declarator.id.name;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Handles object property nodes (methods in object literals)
|
|
79
|
+
* @param {Object} property - Property node
|
|
80
|
+
* @param {Object} node - Original node being analyzed
|
|
81
|
+
* @returns {string|null} Function name or null
|
|
82
|
+
*/
|
|
83
|
+
function handleObjectProperty(property, node) {
|
|
84
|
+
if (property.value === node || isFunctionNode(property.value)) {
|
|
85
|
+
return property.key.name || property.key.value || 'anonymous';
|
|
86
|
+
}
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Handles named export declarations
|
|
92
|
+
* @param {Object} exportNode - ExportNamedDeclaration node
|
|
93
|
+
* @returns {string|null} Function name or null
|
|
94
|
+
*/
|
|
95
|
+
function handleNamedExport(exportNode) {
|
|
96
|
+
if (!exportNode.declaration || !exportNode.declaration.declarations) {
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const declaration = exportNode.declaration.declarations[0];
|
|
101
|
+
if (declaration && isFunctionNode(declaration.init)) {
|
|
102
|
+
return declaration.id.name;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Checks if a node is a function (arrow function or function expression)
|
|
110
|
+
* @param {Object} node - AST node to check
|
|
111
|
+
* @returns {boolean}
|
|
112
|
+
*/
|
|
113
|
+
function isFunctionNode(node) {
|
|
114
|
+
return node && (
|
|
115
|
+
node.type === NODE_TYPES.ARROW_FUNCTION ||
|
|
116
|
+
node.type === NODE_TYPES.FUNCTION_EXPRESSION ||
|
|
117
|
+
node.type === NODE_TYPES.FUNCTION_DECLARATION
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
module.exports = {
|
|
122
|
+
findWrappingFunction
|
|
123
|
+
};
|