@flisk/analyze-tracking 0.8.2 → 0.8.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/analyze/index.js +69 -36
- package/src/analyze/ruby/traversal.js +45 -23
- package/src/analyze/typescript/index.js +135 -17
- package/src/analyze/typescript/parser.js +129 -42
package/package.json
CHANGED
package/src/analyze/index.js
CHANGED
|
@@ -8,32 +8,59 @@ const path = require('path');
|
|
|
8
8
|
const { parseCustomFunctionSignature } = require('./utils/customFunctionParser');
|
|
9
9
|
const { getAllFiles } = require('../utils/fileProcessor');
|
|
10
10
|
const { analyzeJsFile } = require('./javascript');
|
|
11
|
-
const {
|
|
11
|
+
const { analyzeTsFiles } = require('./typescript');
|
|
12
12
|
const { analyzePythonFile } = require('./python');
|
|
13
13
|
const { analyzeRubyFile } = require('./ruby');
|
|
14
14
|
const { analyzeGoFile } = require('./go');
|
|
15
15
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
16
|
+
/**
|
|
17
|
+
* Adds an event to the events collection, merging properties if event already exists
|
|
18
|
+
* @param {Object} allEvents - Collection of all events
|
|
19
|
+
* @param {Object} event - Event to add
|
|
20
|
+
* @param {string} baseDir - Base directory for relative path calculation
|
|
21
|
+
*/
|
|
22
|
+
function addEventToCollection(allEvents, event, baseDir) {
|
|
23
|
+
const relativeFilePath = path.relative(baseDir, event.filePath);
|
|
24
|
+
|
|
25
|
+
const implementation = {
|
|
26
|
+
path: relativeFilePath,
|
|
27
|
+
line: event.line,
|
|
28
|
+
function: event.functionName,
|
|
29
|
+
destination: event.source
|
|
30
|
+
};
|
|
20
31
|
|
|
21
|
-
|
|
32
|
+
if (!allEvents[event.eventName]) {
|
|
33
|
+
allEvents[event.eventName] = {
|
|
34
|
+
implementations: [implementation],
|
|
35
|
+
properties: event.properties,
|
|
36
|
+
};
|
|
37
|
+
} else {
|
|
38
|
+
allEvents[event.eventName].implementations.push(implementation);
|
|
39
|
+
allEvents[event.eventName].properties = {
|
|
40
|
+
...allEvents[event.eventName].properties,
|
|
41
|
+
...event.properties,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
}
|
|
22
45
|
|
|
46
|
+
/**
|
|
47
|
+
* Processes all files that are not TypeScript files
|
|
48
|
+
* @param {Array<string>} files - Array of file paths
|
|
49
|
+
* @param {Object} allEvents - Collection to add events to
|
|
50
|
+
* @param {string} baseDir - Base directory for relative paths
|
|
51
|
+
* @param {Array} customFunctionSignatures - Custom function signatures to detect
|
|
52
|
+
*/
|
|
53
|
+
async function processFiles(files, allEvents, baseDir, customFunctionSignatures) {
|
|
23
54
|
for (const file of files) {
|
|
24
55
|
let events = [];
|
|
25
56
|
|
|
26
57
|
const isJsFile = /\.(jsx?)$/.test(file);
|
|
27
|
-
const isTsFile = /\.(tsx?)$/.test(file);
|
|
28
58
|
const isPythonFile = /\.(py)$/.test(file);
|
|
29
59
|
const isRubyFile = /\.(rb)$/.test(file);
|
|
30
60
|
const isGoFile = /\.(go)$/.test(file);
|
|
31
61
|
|
|
32
62
|
if (isJsFile) {
|
|
33
63
|
events = analyzeJsFile(file, customFunctionSignatures);
|
|
34
|
-
} else if (isTsFile) {
|
|
35
|
-
// Pass null program so analyzeTsFile will create a per-file program using the file's nearest tsconfig.json
|
|
36
|
-
events = analyzeTsFile(file, null, customFunctionSignatures);
|
|
37
64
|
} else if (isPythonFile) {
|
|
38
65
|
events = await analyzePythonFile(file, customFunctionSignatures);
|
|
39
66
|
} else if (isRubyFile) {
|
|
@@ -41,36 +68,42 @@ async function analyzeDirectory(dirPath, customFunctions) {
|
|
|
41
68
|
} else if (isGoFile) {
|
|
42
69
|
events = await analyzeGoFile(file, customFunctionSignatures);
|
|
43
70
|
} else {
|
|
44
|
-
continue;
|
|
71
|
+
continue; // Skip unsupported file types
|
|
45
72
|
}
|
|
46
73
|
|
|
47
|
-
events.forEach(
|
|
48
|
-
|
|
74
|
+
events.forEach(event => addEventToCollection(allEvents, event, baseDir));
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async function analyzeDirectory(dirPath, customFunctions) {
|
|
79
|
+
const allEvents = {};
|
|
80
|
+
|
|
81
|
+
const customFunctionSignatures = (customFunctions?.length > 0)
|
|
82
|
+
? customFunctions.map(parseCustomFunctionSignature)
|
|
83
|
+
: null;
|
|
84
|
+
|
|
85
|
+
const files = getAllFiles(dirPath);
|
|
86
|
+
|
|
87
|
+
// Separate TypeScript files from others for optimized processing
|
|
88
|
+
const tsFiles = [];
|
|
89
|
+
const otherFiles = [];
|
|
90
|
+
|
|
91
|
+
for (const file of files) {
|
|
92
|
+
const isTsFile = /\.(tsx?)$/.test(file);
|
|
93
|
+
if (isTsFile) {
|
|
94
|
+
tsFiles.push(file);
|
|
95
|
+
} else {
|
|
96
|
+
otherFiles.push(file);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
49
99
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
implementations: [{
|
|
53
|
-
path: relativeFilePath,
|
|
54
|
-
line: event.line,
|
|
55
|
-
function: event.functionName,
|
|
56
|
-
destination: event.source
|
|
57
|
-
}],
|
|
58
|
-
properties: event.properties,
|
|
59
|
-
};
|
|
60
|
-
} else {
|
|
61
|
-
allEvents[event.eventName].implementations.push({
|
|
62
|
-
path: relativeFilePath,
|
|
63
|
-
line: event.line,
|
|
64
|
-
function: event.functionName,
|
|
65
|
-
destination: event.source
|
|
66
|
-
});
|
|
100
|
+
// First process non-TypeScript files
|
|
101
|
+
await processFiles(otherFiles, allEvents, dirPath, customFunctionSignatures);
|
|
67
102
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
}
|
|
73
|
-
});
|
|
103
|
+
// Process TypeScript files with optimized batch processing
|
|
104
|
+
if (tsFiles.length > 0) {
|
|
105
|
+
const tsEvents = analyzeTsFiles(tsFiles, customFunctionSignatures);
|
|
106
|
+
tsEvents.forEach(event => addEventToCollection(allEvents, event, dirPath));
|
|
74
107
|
}
|
|
75
108
|
|
|
76
109
|
return allEvents;
|
|
@@ -3,6 +3,10 @@
|
|
|
3
3
|
* @module analyze/ruby/traversal
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
+
|
|
7
|
+
// Prevent infinite recursion in AST traversal
|
|
8
|
+
const MAX_RECURSION_DEPTH = 20;
|
|
9
|
+
|
|
6
10
|
/**
|
|
7
11
|
* Finds the wrapping function for a given node
|
|
8
12
|
* @param {Object} node - The current AST node
|
|
@@ -33,8 +37,9 @@ async function findWrappingFunction(node, ancestors) {
|
|
|
33
37
|
* @param {Object} node - The current AST node
|
|
34
38
|
* @param {Function} nodeVisitor - Function to call for each node
|
|
35
39
|
* @param {Array} ancestors - The ancestor nodes stack
|
|
40
|
+
* @param {number} depth - Current recursion depth to prevent infinite loops
|
|
36
41
|
*/
|
|
37
|
-
async function traverseNode(node, nodeVisitor, ancestors = []) {
|
|
42
|
+
async function traverseNode(node, nodeVisitor, ancestors = [], depth = 0) {
|
|
38
43
|
const {
|
|
39
44
|
ProgramNode,
|
|
40
45
|
StatementsNode,
|
|
@@ -53,6 +58,16 @@ async function traverseNode(node, nodeVisitor, ancestors = []) {
|
|
|
53
58
|
|
|
54
59
|
if (!node) return;
|
|
55
60
|
|
|
61
|
+
// Prevent infinite recursion with depth limit
|
|
62
|
+
if (depth > MAX_RECURSION_DEPTH) {
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Check for circular references - if this node is already in ancestors, skip it
|
|
67
|
+
if (ancestors.includes(node)) {
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
|
|
56
71
|
ancestors.push(node);
|
|
57
72
|
|
|
58
73
|
// Call the visitor for this node
|
|
@@ -62,71 +77,71 @@ async function traverseNode(node, nodeVisitor, ancestors = []) {
|
|
|
62
77
|
|
|
63
78
|
// Visit all child nodes based on node type
|
|
64
79
|
if (node instanceof ProgramNode) {
|
|
65
|
-
await traverseNode(node.statements, nodeVisitor, ancestors);
|
|
80
|
+
await traverseNode(node.statements, nodeVisitor, ancestors, depth + 1);
|
|
66
81
|
} else if (node instanceof StatementsNode) {
|
|
67
82
|
for (const child of node.body) {
|
|
68
|
-
await traverseNode(child, nodeVisitor, ancestors);
|
|
83
|
+
await traverseNode(child, nodeVisitor, ancestors, depth + 1);
|
|
69
84
|
}
|
|
70
85
|
} else if (node instanceof ClassNode) {
|
|
71
86
|
if (node.body) {
|
|
72
|
-
await traverseNode(node.body, nodeVisitor, ancestors);
|
|
87
|
+
await traverseNode(node.body, nodeVisitor, ancestors, depth + 1);
|
|
73
88
|
}
|
|
74
89
|
} else if (node instanceof ModuleNode) {
|
|
75
90
|
if (node.body) {
|
|
76
|
-
await traverseNode(node.body, nodeVisitor, ancestors);
|
|
91
|
+
await traverseNode(node.body, nodeVisitor, ancestors, depth + 1);
|
|
77
92
|
}
|
|
78
93
|
} else if (node instanceof DefNode) {
|
|
79
94
|
if (node.body) {
|
|
80
|
-
await traverseNode(node.body, nodeVisitor, ancestors);
|
|
95
|
+
await traverseNode(node.body, nodeVisitor, ancestors, depth + 1);
|
|
81
96
|
}
|
|
82
97
|
} else if (node instanceof IfNode) {
|
|
83
98
|
if (node.statements) {
|
|
84
|
-
await traverseNode(node.statements, nodeVisitor, ancestors);
|
|
99
|
+
await traverseNode(node.statements, nodeVisitor, ancestors, depth + 1);
|
|
85
100
|
}
|
|
86
101
|
if (node.subsequent) {
|
|
87
|
-
await traverseNode(node.subsequent, nodeVisitor, ancestors);
|
|
102
|
+
await traverseNode(node.subsequent, nodeVisitor, ancestors, depth + 1);
|
|
88
103
|
}
|
|
89
104
|
} else if (node instanceof BlockNode) {
|
|
90
105
|
if (node.body) {
|
|
91
|
-
await traverseNode(node.body, nodeVisitor, ancestors);
|
|
106
|
+
await traverseNode(node.body, nodeVisitor, ancestors, depth + 1);
|
|
92
107
|
}
|
|
93
108
|
} else if (node instanceof ArgumentsNode) {
|
|
94
109
|
const argsList = node.arguments || [];
|
|
95
110
|
for (const arg of argsList) {
|
|
96
|
-
await traverseNode(arg, nodeVisitor, ancestors);
|
|
111
|
+
await traverseNode(arg, nodeVisitor, ancestors, depth + 1);
|
|
97
112
|
}
|
|
98
113
|
} else if (node instanceof HashNode) {
|
|
99
114
|
for (const element of node.elements) {
|
|
100
|
-
await traverseNode(element, nodeVisitor, ancestors);
|
|
115
|
+
await traverseNode(element, nodeVisitor, ancestors, depth + 1);
|
|
101
116
|
}
|
|
102
117
|
} else if (node instanceof AssocNode) {
|
|
103
|
-
await traverseNode(node.key, nodeVisitor, ancestors);
|
|
104
|
-
await traverseNode(node.value, nodeVisitor, ancestors);
|
|
118
|
+
await traverseNode(node.key, nodeVisitor, ancestors, depth + 1);
|
|
119
|
+
await traverseNode(node.value, nodeVisitor, ancestors, depth + 1);
|
|
105
120
|
} else if (node instanceof CaseNode) {
|
|
106
121
|
// Traverse through each 'when' clause and the optional else clause
|
|
107
122
|
const whenClauses = node.whens || node.conditions || node.when_bodies || [];
|
|
108
123
|
for (const when of whenClauses) {
|
|
109
|
-
await traverseNode(when, nodeVisitor, ancestors);
|
|
124
|
+
await traverseNode(when, nodeVisitor, ancestors, depth + 1);
|
|
110
125
|
}
|
|
111
126
|
if (node.else_) {
|
|
112
|
-
await traverseNode(node.else_, nodeVisitor, ancestors);
|
|
127
|
+
await traverseNode(node.else_, nodeVisitor, ancestors, depth + 1);
|
|
113
128
|
} else if (node.elseBody) {
|
|
114
|
-
await traverseNode(node.elseBody, nodeVisitor, ancestors);
|
|
129
|
+
await traverseNode(node.elseBody, nodeVisitor, ancestors, depth + 1);
|
|
115
130
|
}
|
|
116
131
|
} else if (node instanceof WhenNode) {
|
|
117
132
|
// Handle a single when clause: traverse its condition(s) and body
|
|
118
133
|
if (Array.isArray(node.conditions)) {
|
|
119
134
|
for (const cond of node.conditions) {
|
|
120
|
-
await traverseNode(cond, nodeVisitor, ancestors);
|
|
135
|
+
await traverseNode(cond, nodeVisitor, ancestors, depth + 1);
|
|
121
136
|
}
|
|
122
137
|
} else if (node.conditions) {
|
|
123
|
-
await traverseNode(node.conditions, nodeVisitor, ancestors);
|
|
138
|
+
await traverseNode(node.conditions, nodeVisitor, ancestors, depth + 1);
|
|
124
139
|
}
|
|
125
140
|
if (node.statements) {
|
|
126
|
-
await traverseNode(node.statements, nodeVisitor, ancestors);
|
|
141
|
+
await traverseNode(node.statements, nodeVisitor, ancestors, depth + 1);
|
|
127
142
|
}
|
|
128
143
|
if (node.next) {
|
|
129
|
-
await traverseNode(node.next, nodeVisitor, ancestors);
|
|
144
|
+
await traverseNode(node.next, nodeVisitor, ancestors, depth + 1);
|
|
130
145
|
}
|
|
131
146
|
} else {
|
|
132
147
|
// Generic fallback: iterate over enumerable properties to find nested nodes
|
|
@@ -136,9 +151,16 @@ async function traverseNode(node, nodeVisitor, ancestors = []) {
|
|
|
136
151
|
|
|
137
152
|
const visitChild = async (child) => {
|
|
138
153
|
if (child && typeof child === 'object') {
|
|
139
|
-
//
|
|
140
|
-
|
|
141
|
-
|
|
154
|
+
// More restrictive check: ensure it's actually a Prism AST node
|
|
155
|
+
// Check for specific Prism node indicators and avoid circular references
|
|
156
|
+
if (
|
|
157
|
+
child.location &&
|
|
158
|
+
child.constructor &&
|
|
159
|
+
child.constructor.name &&
|
|
160
|
+
child.constructor.name.endsWith('Node') &&
|
|
161
|
+
!ancestors.includes(child)
|
|
162
|
+
) {
|
|
163
|
+
await traverseNode(child, nodeVisitor, ancestors, depth + 1);
|
|
142
164
|
}
|
|
143
165
|
}
|
|
144
166
|
};
|
|
@@ -3,40 +3,130 @@
|
|
|
3
3
|
* @module analyze/typescript
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
const { getProgram, findTrackingEvents, ProgramError, SourceFileError } = require('./parser');
|
|
6
|
+
const { getProgram, findTrackingEvents, ProgramError, SourceFileError, DEFAULT_COMPILER_OPTIONS } = require('./parser');
|
|
7
|
+
const ts = require('typescript');
|
|
8
|
+
const path = require('path');
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Creates a standalone TypeScript program for a single file
|
|
12
|
+
* This is used as a fallback when the main program can't resolve the file
|
|
13
|
+
* @param {string} filePath - Path to the TypeScript file
|
|
14
|
+
* @returns {Object} TypeScript program
|
|
15
|
+
*/
|
|
16
|
+
function createStandaloneProgram(filePath) {
|
|
17
|
+
const compilerOptions = {
|
|
18
|
+
...DEFAULT_COMPILER_OPTIONS,
|
|
19
|
+
// We intentionally allow module resolution here so that imported constants
|
|
20
|
+
// (e.g. event name strings defined in a sibling file) can be followed by the
|
|
21
|
+
// TypeScript compiler.
|
|
22
|
+
isolatedModules: true
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
return ts.createProgram([filePath], compilerOptions);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Deduplicates events based on source, eventName, line, and functionName
|
|
30
|
+
* @param {Array<Object>} events - Array of events to deduplicate
|
|
31
|
+
* @returns {Array<Object>} Deduplicated events
|
|
32
|
+
*/
|
|
33
|
+
function deduplicateEvents(events) {
|
|
34
|
+
const uniqueEvents = new Map();
|
|
35
|
+
|
|
36
|
+
for (const event of events) {
|
|
37
|
+
const key = `${event.source}|${event.eventName}|${event.line}|${event.functionName}`;
|
|
38
|
+
if (!uniqueEvents.has(key)) {
|
|
39
|
+
uniqueEvents.set(key, event);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return Array.from(uniqueEvents.values());
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Attempts to analyze a file using a standalone program as fallback
|
|
48
|
+
* @param {string} filePath - Path to the TypeScript file
|
|
49
|
+
* @param {Array} customFunctionSignatures - Custom function signatures to detect
|
|
50
|
+
* @returns {Array<Object>} Array of events or empty array if failed
|
|
51
|
+
*/
|
|
52
|
+
function tryStandaloneAnalysis(filePath, customFunctionSignatures) {
|
|
53
|
+
try {
|
|
54
|
+
console.warn(`Unable to resolve ${filePath} in main program. Attempting standalone analysis.`);
|
|
55
|
+
|
|
56
|
+
const standaloneProgram = createStandaloneProgram(filePath);
|
|
57
|
+
const sourceFile = standaloneProgram.getSourceFile(filePath);
|
|
58
|
+
|
|
59
|
+
if (!sourceFile) {
|
|
60
|
+
console.warn(`Standalone analysis failed: could not get source file for ${filePath}`);
|
|
61
|
+
return [];
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const checker = standaloneProgram.getTypeChecker();
|
|
65
|
+
const events = findTrackingEvents(sourceFile, checker, filePath, customFunctionSignatures || []);
|
|
66
|
+
|
|
67
|
+
return deduplicateEvents(events);
|
|
68
|
+
} catch (standaloneError) {
|
|
69
|
+
console.warn(`Standalone analysis failed for ${filePath}: ${standaloneError.message}`);
|
|
70
|
+
return [];
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Gets or creates a cached TypeScript program for efficient reuse
|
|
76
|
+
* @param {string} filePath - Path to the TypeScript file
|
|
77
|
+
* @param {Map} programCache - Map of tsconfig paths to programs
|
|
78
|
+
* @returns {Object} TypeScript program
|
|
79
|
+
*/
|
|
80
|
+
function getCachedTsProgram(filePath, programCache) {
|
|
81
|
+
// Locate nearest tsconfig.json (may be undefined)
|
|
82
|
+
const searchPath = path.dirname(filePath);
|
|
83
|
+
const configPath = ts.findConfigFile(searchPath, ts.sys.fileExists, 'tsconfig.json');
|
|
84
|
+
|
|
85
|
+
// We only cache when a tsconfig.json exists because the resulting program
|
|
86
|
+
// represents an entire project. If no config is present we build a
|
|
87
|
+
// stand-alone program that should not be reused for other files – otherwise
|
|
88
|
+
// later files would be missing from the program (which is precisely what
|
|
89
|
+
// caused the regression we are fixing).
|
|
90
|
+
const shouldCache = Boolean(configPath);
|
|
91
|
+
const cacheKey = configPath; // undefined when shouldCache is false
|
|
92
|
+
|
|
93
|
+
if (shouldCache && programCache.has(cacheKey)) {
|
|
94
|
+
return programCache.get(cacheKey);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const program = getProgram(filePath, null);
|
|
98
|
+
|
|
99
|
+
if (shouldCache) {
|
|
100
|
+
programCache.set(cacheKey, program);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return program;
|
|
104
|
+
}
|
|
7
105
|
|
|
8
106
|
/**
|
|
9
107
|
* Analyzes a TypeScript file for analytics tracking calls
|
|
10
108
|
* @param {string} filePath - Path to the TypeScript file to analyze
|
|
11
109
|
* @param {Object} [program] - Optional existing TypeScript program to reuse
|
|
12
|
-
* @param {
|
|
110
|
+
* @param {Array} [customFunctionSignatures] - Optional custom function signatures to detect
|
|
13
111
|
* @returns {Array<Object>} Array of tracking events found in the file
|
|
14
112
|
*/
|
|
15
113
|
function analyzeTsFile(filePath, program = null, customFunctionSignatures = null) {
|
|
16
114
|
try {
|
|
17
|
-
// Get or create TypeScript program
|
|
115
|
+
// Get or create TypeScript program
|
|
18
116
|
const tsProgram = getProgram(filePath, program);
|
|
19
117
|
|
|
20
118
|
// Get source file from program
|
|
21
119
|
const sourceFile = tsProgram.getSourceFile(filePath);
|
|
22
120
|
if (!sourceFile) {
|
|
23
|
-
|
|
121
|
+
// Try standalone analysis as fallback
|
|
122
|
+
return tryStandaloneAnalysis(filePath, customFunctionSignatures);
|
|
24
123
|
}
|
|
25
124
|
|
|
26
|
-
// Get type checker
|
|
125
|
+
// Get type checker and find tracking events
|
|
27
126
|
const checker = tsProgram.getTypeChecker();
|
|
28
|
-
|
|
29
|
-
// Single-pass collection covering built-in + all custom configs
|
|
30
127
|
const events = findTrackingEvents(sourceFile, checker, filePath, customFunctionSignatures || []);
|
|
31
128
|
|
|
32
|
-
|
|
33
|
-
const unique = new Map();
|
|
34
|
-
for (const evt of events) {
|
|
35
|
-
const key = `${evt.source}|${evt.eventName}|${evt.line}|${evt.functionName}`;
|
|
36
|
-
if (!unique.has(key)) unique.set(key, evt);
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
return Array.from(unique.values());
|
|
129
|
+
return deduplicateEvents(events);
|
|
40
130
|
|
|
41
131
|
} catch (error) {
|
|
42
132
|
if (error instanceof ProgramError) {
|
|
@@ -46,9 +136,37 @@ function analyzeTsFile(filePath, program = null, customFunctionSignatures = null
|
|
|
46
136
|
} else {
|
|
47
137
|
console.error(`Error analyzing TypeScript file ${filePath}: ${error.message}`);
|
|
48
138
|
}
|
|
139
|
+
|
|
140
|
+
return [];
|
|
49
141
|
}
|
|
142
|
+
}
|
|
50
143
|
|
|
51
|
-
|
|
144
|
+
/**
|
|
145
|
+
* Analyzes multiple TypeScript files with program reuse for better performance
|
|
146
|
+
* @param {Array<string>} tsFiles - Array of TypeScript file paths
|
|
147
|
+
* @param {Array} customFunctionSignatures - Custom function signatures to detect
|
|
148
|
+
* @returns {Array<Object>} Array of all tracking events found across all files
|
|
149
|
+
*/
|
|
150
|
+
function analyzeTsFiles(tsFiles, customFunctionSignatures) {
|
|
151
|
+
const allEvents = [];
|
|
152
|
+
const tsProgramCache = new Map(); // tsconfig path -> program
|
|
153
|
+
|
|
154
|
+
for (const file of tsFiles) {
|
|
155
|
+
try {
|
|
156
|
+
// Use cached program or create new one
|
|
157
|
+
const program = getCachedTsProgram(file, tsProgramCache);
|
|
158
|
+
const events = analyzeTsFile(file, program, customFunctionSignatures);
|
|
159
|
+
|
|
160
|
+
allEvents.push(...events);
|
|
161
|
+
} catch (error) {
|
|
162
|
+
console.warn(`Error processing TypeScript file ${file}: ${error.message}`);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return allEvents;
|
|
52
167
|
}
|
|
53
168
|
|
|
54
|
-
module.exports = {
|
|
169
|
+
module.exports = {
|
|
170
|
+
analyzeTsFile,
|
|
171
|
+
analyzeTsFiles
|
|
172
|
+
};
|
|
@@ -32,6 +32,103 @@ class SourceFileError extends Error {
|
|
|
32
32
|
}
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
+
/**
|
|
36
|
+
* Default TypeScript compiler options for analysis
|
|
37
|
+
*/
|
|
38
|
+
const DEFAULT_COMPILER_OPTIONS = {
|
|
39
|
+
target: ts.ScriptTarget.Latest,
|
|
40
|
+
module: ts.ModuleKind.CommonJS,
|
|
41
|
+
allowJs: true,
|
|
42
|
+
checkJs: false,
|
|
43
|
+
noEmit: true,
|
|
44
|
+
jsx: ts.JsxEmit.Preserve,
|
|
45
|
+
moduleResolution: ts.ModuleResolutionKind.NodeJs,
|
|
46
|
+
allowSyntheticDefaultImports: true,
|
|
47
|
+
esModuleInterop: true,
|
|
48
|
+
skipLibCheck: true
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Maximum number of files to include in TypeScript program for performance
|
|
53
|
+
*/
|
|
54
|
+
const MAX_FILES_THRESHOLD = 10000;
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Attempts to parse tsconfig.json and extract compiler options and file names
|
|
58
|
+
* @param {string} configPath - Path to tsconfig.json
|
|
59
|
+
* @returns {Object|null} Parsed config with options and fileNames, or null if failed
|
|
60
|
+
*/
|
|
61
|
+
function parseTsConfig(configPath) {
|
|
62
|
+
try {
|
|
63
|
+
const readResult = ts.readConfigFile(configPath, ts.sys.readFile);
|
|
64
|
+
if (readResult.error || !readResult.config) {
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const parseResult = ts.parseJsonConfigFileContent(
|
|
69
|
+
readResult.config,
|
|
70
|
+
ts.sys,
|
|
71
|
+
path.dirname(configPath)
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
if (parseResult.errors && parseResult.errors.length > 0) {
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return {
|
|
79
|
+
options: parseResult.options,
|
|
80
|
+
fileNames: parseResult.fileNames
|
|
81
|
+
};
|
|
82
|
+
} catch (error) {
|
|
83
|
+
console.warn(`Failed to parse tsconfig.json at ${configPath}. Error: ${error.message}`);
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Determines the appropriate files to include in the TypeScript program
|
|
90
|
+
* @param {string} filePath - Target file path
|
|
91
|
+
* @param {string|null} configPath - Path to tsconfig.json if found
|
|
92
|
+
* @returns {Object} Configuration with compilerOptions and rootNames
|
|
93
|
+
*/
|
|
94
|
+
function getProgramConfiguration(filePath, configPath) {
|
|
95
|
+
let compilerOptions = { ...DEFAULT_COMPILER_OPTIONS };
|
|
96
|
+
let rootNames = [filePath];
|
|
97
|
+
|
|
98
|
+
if (!configPath) {
|
|
99
|
+
return { compilerOptions, rootNames };
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const config = parseTsConfig(configPath);
|
|
103
|
+
if (!config) {
|
|
104
|
+
console.warn(`Failed to parse tsconfig.json at ${configPath}. Analyzing ${filePath} in isolation.`);
|
|
105
|
+
return { compilerOptions, rootNames };
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Inherit compiler options from tsconfig
|
|
109
|
+
compilerOptions = { ...compilerOptions, ...config.options };
|
|
110
|
+
|
|
111
|
+
// Determine file inclusion strategy based on project size
|
|
112
|
+
const projectFileCount = config.fileNames.length;
|
|
113
|
+
|
|
114
|
+
if (projectFileCount > 0 && projectFileCount <= MAX_FILES_THRESHOLD) {
|
|
115
|
+
// Small to medium project: include all files for better type checking
|
|
116
|
+
rootNames = [...config.fileNames];
|
|
117
|
+
if (!rootNames.includes(filePath)) {
|
|
118
|
+
rootNames.push(filePath);
|
|
119
|
+
}
|
|
120
|
+
} else if (projectFileCount > MAX_FILES_THRESHOLD) {
|
|
121
|
+
// Large project: only include the target file to avoid performance issues
|
|
122
|
+
console.warn(
|
|
123
|
+
`Large TypeScript project detected (${projectFileCount} files). ` +
|
|
124
|
+
`Analyzing ${filePath} in isolation for performance.`
|
|
125
|
+
);
|
|
126
|
+
rootNames = [filePath];
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return { compilerOptions, rootNames };
|
|
130
|
+
}
|
|
131
|
+
|
|
35
132
|
/**
|
|
36
133
|
* Gets or creates a TypeScript program for analysis
|
|
37
134
|
* @param {string} filePath - Path to the TypeScript file
|
|
@@ -45,38 +142,15 @@ function getProgram(filePath, existingProgram) {
|
|
|
45
142
|
}
|
|
46
143
|
|
|
47
144
|
try {
|
|
48
|
-
//
|
|
145
|
+
// Find the nearest tsconfig.json
|
|
49
146
|
const searchPath = path.dirname(filePath);
|
|
50
147
|
const configPath = ts.findConfigFile(searchPath, ts.sys.fileExists, 'tsconfig.json');
|
|
51
148
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
module: ts.ModuleKind.CommonJS,
|
|
55
|
-
allowJs: true,
|
|
56
|
-
checkJs: false,
|
|
57
|
-
noEmit: true,
|
|
58
|
-
jsx: ts.JsxEmit.Preserve
|
|
59
|
-
};
|
|
60
|
-
let rootNames = [filePath];
|
|
61
|
-
|
|
62
|
-
if (configPath) {
|
|
63
|
-
// Read and parse the tsconfig.json
|
|
64
|
-
const readResult = ts.readConfigFile(configPath, ts.sys.readFile);
|
|
65
|
-
if (!readResult.error && readResult.config) {
|
|
66
|
-
const parseResult = ts.parseJsonConfigFileContent(
|
|
67
|
-
readResult.config,
|
|
68
|
-
ts.sys,
|
|
69
|
-
path.dirname(configPath)
|
|
70
|
-
);
|
|
71
|
-
if (!parseResult.errors || parseResult.errors.length === 0) {
|
|
72
|
-
compilerOptions = { ...compilerOptions, ...parseResult.options };
|
|
73
|
-
rootNames = parseResult.fileNames.length > 0 ? parseResult.fileNames : rootNames;
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
}
|
|
149
|
+
// Get program configuration
|
|
150
|
+
const { compilerOptions, rootNames } = getProgramConfiguration(filePath, configPath);
|
|
77
151
|
|
|
78
|
-
|
|
79
|
-
return
|
|
152
|
+
// Create and return the TypeScript program
|
|
153
|
+
return ts.createProgram(rootNames, compilerOptions);
|
|
80
154
|
} catch (error) {
|
|
81
155
|
throw new ProgramError(filePath, error);
|
|
82
156
|
}
|
|
@@ -94,27 +168,37 @@ function findTrackingEvents(sourceFile, checker, filePath, customConfigs = []) {
|
|
|
94
168
|
const events = [];
|
|
95
169
|
|
|
96
170
|
/**
|
|
97
|
-
*
|
|
98
|
-
*
|
|
171
|
+
* Tests if a CallExpression matches a custom function name
|
|
172
|
+
* @param {Object} callNode - The call expression node
|
|
173
|
+
* @param {string} functionName - Function name to match
|
|
174
|
+
* @returns {boolean} True if matches
|
|
99
175
|
*/
|
|
100
|
-
|
|
101
|
-
if (!
|
|
176
|
+
function matchesCustomFunction(callNode, functionName) {
|
|
177
|
+
if (!functionName || !callNode.expression) {
|
|
178
|
+
return false;
|
|
179
|
+
}
|
|
180
|
+
|
|
102
181
|
try {
|
|
103
|
-
return callNode.expression
|
|
182
|
+
return callNode.expression.getText() === functionName;
|
|
104
183
|
} catch {
|
|
105
184
|
return false;
|
|
106
185
|
}
|
|
107
|
-
}
|
|
186
|
+
}
|
|
108
187
|
|
|
188
|
+
/**
|
|
189
|
+
* Recursively visits AST nodes to find tracking calls
|
|
190
|
+
* @param {Object} node - Current AST node
|
|
191
|
+
*/
|
|
109
192
|
function visit(node) {
|
|
110
193
|
try {
|
|
111
194
|
if (ts.isCallExpression(node)) {
|
|
112
|
-
let
|
|
195
|
+
let matchedCustomConfig = null;
|
|
113
196
|
|
|
197
|
+
// Check for custom function matches
|
|
114
198
|
if (Array.isArray(customConfigs) && customConfigs.length > 0) {
|
|
115
|
-
for (const
|
|
116
|
-
if (
|
|
117
|
-
|
|
199
|
+
for (const config of customConfigs) {
|
|
200
|
+
if (config && matchesCustomFunction(node, config.functionName)) {
|
|
201
|
+
matchedCustomConfig = config;
|
|
118
202
|
break;
|
|
119
203
|
}
|
|
120
204
|
}
|
|
@@ -125,9 +209,12 @@ function findTrackingEvents(sourceFile, checker, filePath, customConfigs = []) {
|
|
|
125
209
|
sourceFile,
|
|
126
210
|
checker,
|
|
127
211
|
filePath,
|
|
128
|
-
|
|
212
|
+
matchedCustomConfig
|
|
129
213
|
);
|
|
130
|
-
|
|
214
|
+
|
|
215
|
+
if (event) {
|
|
216
|
+
events.push(event);
|
|
217
|
+
}
|
|
131
218
|
}
|
|
132
219
|
|
|
133
220
|
ts.forEachChild(node, visit);
|
|
@@ -137,7 +224,6 @@ function findTrackingEvents(sourceFile, checker, filePath, customConfigs = []) {
|
|
|
137
224
|
}
|
|
138
225
|
|
|
139
226
|
ts.forEachChild(sourceFile, visit);
|
|
140
|
-
|
|
141
227
|
return events;
|
|
142
228
|
}
|
|
143
229
|
|
|
@@ -172,5 +258,6 @@ module.exports = {
|
|
|
172
258
|
getProgram,
|
|
173
259
|
findTrackingEvents,
|
|
174
260
|
ProgramError,
|
|
175
|
-
SourceFileError
|
|
261
|
+
SourceFileError,
|
|
262
|
+
DEFAULT_COMPILER_OPTIONS
|
|
176
263
|
};
|