Skip to content

Commit e9791ff

Browse files
committed
fix stuck issue when tsconfig not found
1 parent f9cf091 commit e9791ff

File tree

3 files changed

+325
-95
lines changed

3 files changed

+325
-95
lines changed

src/analyze/index.js

Lines changed: 69 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -8,71 +8,104 @@ const path = require('path');
88
const { parseCustomFunctionSignature } = require('./utils/customFunctionParser');
99
const { getAllFiles } = require('../utils/fileProcessor');
1010
const { analyzeJsFile } = require('./javascript');
11-
const { analyzeTsFile } = require('./typescript');
11+
const { analyzeTsFiles } = require('./typescript');
1212
const { analyzePythonFile } = require('./python');
1313
const { analyzeRubyFile } = require('./ruby');
1414
const { analyzeGoFile } = require('./go');
1515

16-
async function analyzeDirectory(dirPath, customFunctions) {
17-
const allEvents = {};
18-
19-
const customFunctionSignatures = (customFunctions && customFunctions?.length > 0) ? customFunctions.map(parseCustomFunctionSignature) : null;
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+
};
2031

21-
const files = getAllFiles(dirPath);
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+
}
2245

46+
/**
47+
* Processes non-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 processOtherFiles(files, allEvents, baseDir, customFunctionSignatures) {
2354
for (const file of files) {
2455
let events = [];
2556

2657
const isJsFile = /\.(jsx?)$/.test(file);
27-
const isTsFile = /\.(tsx?)$/.test(file);
2858
const isPythonFile = /\.(py)$/.test(file);
2959
const isRubyFile = /\.(rb)$/.test(file);
3060
const isGoFile = /\.(go)$/.test(file);
3161

3262
if (isJsFile) {
3363
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);
3764
} else if (isPythonFile) {
3865
events = await analyzePythonFile(file, customFunctionSignatures);
3966
} else if (isRubyFile) {
4067
events = await analyzeRubyFile(file, customFunctionSignatures);
4168
} else if (isGoFile) {
4269
events = await analyzeGoFile(file, customFunctionSignatures);
4370
} else {
44-
continue;
71+
continue; // Skip unsupported file types
4572
}
4673

47-
events.forEach((event) => {
48-
const relativeFilePath = path.relative(dirPath, event.filePath);
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;
4984

50-
if (!allEvents[event.eventName]) {
51-
allEvents[event.eventName] = {
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-
});
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+
}
6799

68-
allEvents[event.eventName].properties = {
69-
...allEvents[event.eventName].properties,
70-
...event.properties,
71-
};
72-
}
73-
});
100+
// Process TypeScript files with optimized batch processing
101+
if (tsFiles.length > 0) {
102+
const tsEvents = analyzeTsFiles(tsFiles, customFunctionSignatures);
103+
tsEvents.forEach(event => addEventToCollection(allEvents, event, dirPath));
74104
}
75105

106+
// Process remaining file types
107+
await processOtherFiles(otherFiles, allEvents, dirPath, customFunctionSignatures);
108+
76109
return allEvents;
77110
}
78111

src/analyze/typescript/index.js

Lines changed: 127 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -3,40 +3,122 @@
33
* @module analyze/typescript
44
*/
55

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+
noResolve: true, // Don't try to resolve imports - just analyze the file content
20+
isolatedModules: true
21+
};
22+
23+
return ts.createProgram([filePath], compilerOptions);
24+
}
25+
26+
/**
27+
* Deduplicates events based on source, eventName, line, and functionName
28+
* @param {Array<Object>} events - Array of events to deduplicate
29+
* @returns {Array<Object>} Deduplicated events
30+
*/
31+
function deduplicateEvents(events) {
32+
const uniqueEvents = new Map();
33+
34+
for (const event of events) {
35+
const key = `${event.source}|${event.eventName}|${event.line}|${event.functionName}`;
36+
if (!uniqueEvents.has(key)) {
37+
uniqueEvents.set(key, event);
38+
}
39+
}
40+
41+
return Array.from(uniqueEvents.values());
42+
}
43+
44+
/**
45+
* Attempts to analyze a file using a standalone program as fallback
46+
* @param {string} filePath - Path to the TypeScript file
47+
* @param {Array} customFunctionSignatures - Custom function signatures to detect
48+
* @returns {Array<Object>} Array of events or empty array if failed
49+
*/
50+
function tryStandaloneAnalysis(filePath, customFunctionSignatures) {
51+
try {
52+
console.warn(`Unable to resolve ${filePath} in main program. Attempting standalone analysis.`);
53+
54+
const standaloneProgram = createStandaloneProgram(filePath);
55+
const sourceFile = standaloneProgram.getSourceFile(filePath);
56+
57+
if (!sourceFile) {
58+
console.warn(`Standalone analysis failed: could not get source file for ${filePath}`);
59+
return [];
60+
}
61+
62+
const checker = standaloneProgram.getTypeChecker();
63+
const events = findTrackingEvents(sourceFile, checker, filePath, customFunctionSignatures || []);
64+
65+
return deduplicateEvents(events);
66+
} catch (standaloneError) {
67+
console.warn(`Standalone analysis failed for ${filePath}: ${standaloneError.message}`);
68+
return [];
69+
}
70+
}
71+
72+
/**
73+
* Gets or creates a cached TypeScript program for efficient reuse
74+
* @param {string} filePath - Path to the TypeScript file
75+
* @param {Map} programCache - Map of tsconfig paths to programs
76+
* @returns {Object} TypeScript program
77+
*/
78+
function getCachedTsProgram(filePath, programCache) {
79+
// Find the nearest tsconfig.json
80+
const searchPath = path.dirname(filePath);
81+
const configPath = ts.findConfigFile(searchPath, ts.sys.fileExists, 'tsconfig.json');
82+
83+
// Use config path as cache key, or 'standalone' if no config found
84+
const cacheKey = configPath || 'standalone';
85+
86+
// Return cached program if available
87+
if (programCache.has(cacheKey)) {
88+
return programCache.get(cacheKey);
89+
}
90+
91+
// Create new program and cache it
92+
const program = getProgram(filePath, null);
93+
programCache.set(cacheKey, program);
94+
95+
return program;
96+
}
797

898
/**
999
* Analyzes a TypeScript file for analytics tracking calls
10100
* @param {string} filePath - Path to the TypeScript file to analyze
11101
* @param {Object} [program] - Optional existing TypeScript program to reuse
12-
* @param {string} [customFunctionSignature] - Optional custom function signature to detect
102+
* @param {Array} [customFunctionSignatures] - Optional custom function signatures to detect
13103
* @returns {Array<Object>} Array of tracking events found in the file
14104
*/
15105
function analyzeTsFile(filePath, program = null, customFunctionSignatures = null) {
16106
try {
17-
// Get or create TypeScript program (only once)
107+
// Get or create TypeScript program
18108
const tsProgram = getProgram(filePath, program);
19109

20110
// Get source file from program
21111
const sourceFile = tsProgram.getSourceFile(filePath);
22112
if (!sourceFile) {
23-
throw new SourceFileError(filePath);
113+
// Try standalone analysis as fallback
114+
return tryStandaloneAnalysis(filePath, customFunctionSignatures);
24115
}
25116

26-
// Get type checker
117+
// Get type checker and find tracking events
27118
const checker = tsProgram.getTypeChecker();
28-
29-
// Single-pass collection covering built-in + all custom configs
30119
const events = findTrackingEvents(sourceFile, checker, filePath, customFunctionSignatures || []);
31120

32-
// Deduplicate events
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());
121+
return deduplicateEvents(events);
40122

41123
} catch (error) {
42124
if (error instanceof ProgramError) {
@@ -46,9 +128,37 @@ function analyzeTsFile(filePath, program = null, customFunctionSignatures = null
46128
} else {
47129
console.error(`Error analyzing TypeScript file ${filePath}: ${error.message}`);
48130
}
131+
132+
return [];
49133
}
134+
}
50135

51-
return [];
136+
/**
137+
* Analyzes multiple TypeScript files with program reuse for better performance
138+
* @param {Array<string>} tsFiles - Array of TypeScript file paths
139+
* @param {Array} customFunctionSignatures - Custom function signatures to detect
140+
* @returns {Array<Object>} Array of all tracking events found across all files
141+
*/
142+
function analyzeTsFiles(tsFiles, customFunctionSignatures) {
143+
const allEvents = [];
144+
const tsProgramCache = new Map(); // tsconfig path -> program
145+
146+
for (const file of tsFiles) {
147+
try {
148+
// Use cached program or create new one
149+
const program = getCachedTsProgram(file, tsProgramCache);
150+
const events = analyzeTsFile(file, program, customFunctionSignatures);
151+
152+
allEvents.push(...events);
153+
} catch (error) {
154+
console.warn(`Error processing TypeScript file ${file}: ${error.message}`);
155+
}
156+
}
157+
158+
return allEvents;
52159
}
53160

54-
module.exports = { analyzeTsFile };
161+
module.exports = {
162+
analyzeTsFile,
163+
analyzeTsFiles
164+
};

0 commit comments

Comments
 (0)