blast-radius-analyzer 1.2.1

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.
Files changed (49) hide show
  1. package/README.md +108 -0
  2. package/TEST-REPORT.md +379 -0
  3. package/dist/core/AnalysisCache.d.ts +59 -0
  4. package/dist/core/AnalysisCache.js +156 -0
  5. package/dist/core/BlastRadiusAnalyzer.d.ts +99 -0
  6. package/dist/core/BlastRadiusAnalyzer.js +510 -0
  7. package/dist/core/CallStackBuilder.d.ts +63 -0
  8. package/dist/core/CallStackBuilder.js +269 -0
  9. package/dist/core/DataFlowAnalyzer.d.ts +215 -0
  10. package/dist/core/DataFlowAnalyzer.js +1115 -0
  11. package/dist/core/DependencyGraph.d.ts +55 -0
  12. package/dist/core/DependencyGraph.js +541 -0
  13. package/dist/core/ImpactTracer.d.ts +96 -0
  14. package/dist/core/ImpactTracer.js +398 -0
  15. package/dist/core/PropagationTracker.d.ts +73 -0
  16. package/dist/core/PropagationTracker.js +502 -0
  17. package/dist/core/PropertyAccessTracker.d.ts +56 -0
  18. package/dist/core/PropertyAccessTracker.js +281 -0
  19. package/dist/core/SymbolAnalyzer.d.ts +139 -0
  20. package/dist/core/SymbolAnalyzer.js +608 -0
  21. package/dist/core/TypeFlowAnalyzer.d.ts +120 -0
  22. package/dist/core/TypeFlowAnalyzer.js +654 -0
  23. package/dist/core/TypePropagationAnalyzer.d.ts +58 -0
  24. package/dist/core/TypePropagationAnalyzer.js +269 -0
  25. package/dist/index.d.ts +13 -0
  26. package/dist/index.js +952 -0
  27. package/dist/types.d.ts +102 -0
  28. package/dist/types.js +5 -0
  29. package/package.json +39 -0
  30. package/src/core/AnalysisCache.ts +189 -0
  31. package/src/core/CallStackBuilder.ts +345 -0
  32. package/src/core/DataFlowAnalyzer.ts +1403 -0
  33. package/src/core/DependencyGraph.ts +584 -0
  34. package/src/core/ImpactTracer.ts +521 -0
  35. package/src/core/PropagationTracker.ts +630 -0
  36. package/src/core/PropertyAccessTracker.ts +349 -0
  37. package/src/core/SymbolAnalyzer.ts +746 -0
  38. package/src/core/TypeFlowAnalyzer.ts +844 -0
  39. package/src/core/TypePropagationAnalyzer.ts +332 -0
  40. package/src/index.ts +1071 -0
  41. package/src/types.ts +163 -0
  42. package/test-cases/.blast-radius-cache/file-states.json +14 -0
  43. package/test-cases/config.ts +13 -0
  44. package/test-cases/consumer.ts +12 -0
  45. package/test-cases/nested.ts +25 -0
  46. package/test-cases/simple.ts +62 -0
  47. package/test-cases/tsconfig.json +11 -0
  48. package/test-cases/user.ts +32 -0
  49. package/tsconfig.json +16 -0
@@ -0,0 +1,349 @@
1
+ /**
2
+ * PropertyAccessTracker - 属性访问追踪器
3
+ *
4
+ * 追踪函数返回值的属性访问,帮助理解类型传播
5
+ */
6
+
7
+ import * as ts from 'typescript';
8
+ import * as path from 'path';
9
+
10
+ export interface PropertyAccess {
11
+ file: string;
12
+ line: number;
13
+ variableName: string;
14
+ accessChain: string[]; // e.g., ['data', 'total_tasks']
15
+ fullExpression: string;
16
+ codeContext: string; // 完整的代码行
17
+ }
18
+
19
+ export interface CallSiteAnalysis {
20
+ functionName: string;
21
+ file: string;
22
+ line: number;
23
+ callExpression: string;
24
+ returnedTo?: string; // 赋值给的变量
25
+ codeContext: string; // 调用处的完整代码上下文
26
+ propertyAccesses: PropertyAccess[];
27
+ }
28
+
29
+ export class PropertyAccessTracker {
30
+ private program: ts.Program;
31
+ private checker: ts.TypeChecker;
32
+ private projectRoot: string;
33
+
34
+ constructor(projectRoot: string, tsConfigPath: string) {
35
+ this.projectRoot = projectRoot;
36
+
37
+ const configFile = ts.readConfigFile(tsConfigPath, ts.sys.readFile);
38
+ const parsedConfig = ts.parseJsonConfigFileContent(configFile.config, ts.sys, path.dirname(tsConfigPath));
39
+ this.program = ts.createProgram(parsedConfig.fileNames, parsedConfig.options);
40
+ this.checker = this.program.getTypeChecker();
41
+ }
42
+
43
+ /**
44
+ * 分析函数调用的属性访问链
45
+ */
46
+ analyzeFunctionCalls(functionName: string, inFiles?: string[]): CallSiteAnalysis[] {
47
+ const results: CallSiteAnalysis[] = [];
48
+
49
+ for (const sourceFile of this.program.getSourceFiles()) {
50
+ const filePath = sourceFile.fileName;
51
+ if (filePath.includes('node_modules') || filePath.includes('.d.ts')) continue;
52
+
53
+ // 如果指定了文件列表,只分析这些文件
54
+ if (inFiles && inFiles.length > 0) {
55
+ const basename = filePath.replace(/^.*[/\\]/, '');
56
+ if (!inFiles.some(f => filePath.includes(f) || f.includes(basename))) {
57
+ continue;
58
+ }
59
+ }
60
+
61
+ const analysis = this.analyzeFile(sourceFile, functionName);
62
+ if (analysis) {
63
+ results.push(...analysis);
64
+ }
65
+ }
66
+
67
+ return results;
68
+ }
69
+
70
+ /**
71
+ * 分析单个文件
72
+ */
73
+ private analyzeFile(sourceFile: ts.SourceFile, functionName: string): CallSiteAnalysis[] {
74
+ const results: CallSiteAnalysis[] = [];
75
+
76
+ const visit = (node: ts.Node) => {
77
+ // 找函数调用
78
+ if (ts.isCallExpression(node)) {
79
+ const expr = node.expression;
80
+ if (ts.isIdentifier(expr) && expr.text === functionName) {
81
+ const callSite = this.analyzeCallSite(node, sourceFile, functionName);
82
+ if (callSite) {
83
+ results.push(callSite);
84
+ }
85
+ }
86
+ }
87
+ ts.forEachChild(node, visit);
88
+ };
89
+
90
+ visit(sourceFile);
91
+ return results;
92
+ }
93
+
94
+ /**
95
+ * 分析调用点
96
+ */
97
+ private analyzeCallSite(callExpr: ts.CallExpression, sourceFile: ts.SourceFile, functionName: string): CallSiteAnalysis | null {
98
+ const line = ts.getLineAndCharacterOfPosition(sourceFile, callExpr.getStart()).line + 1;
99
+
100
+ // 获取调用行的完整代码上下文
101
+ const sourceText = sourceFile.getFullText();
102
+ const lineStarts = sourceFile.getLineStarts();
103
+ const lineStart = lineStarts[line - 1];
104
+ const lineEnd = lineStarts[line] || sourceText.length;
105
+ const codeContext = sourceText.slice(lineStart, lineEnd).trim();
106
+
107
+ // 检查是否在 Promise.all 中
108
+ let inPromiseAll = false;
109
+ let arrayIndex = -1;
110
+ let parent = callExpr.parent;
111
+
112
+ if (ts.isArrayLiteralExpression(parent)) {
113
+ const elements = parent.elements;
114
+ arrayIndex = elements.indexOf(callExpr);
115
+ const grandParent = parent.parent;
116
+ if (ts.isCallExpression(grandParent) && ts.isIdentifier(grandParent.expression)) {
117
+ if (grandParent.expression.text === 'Promise.all') {
118
+ inPromiseAll = true;
119
+ }
120
+ }
121
+ }
122
+
123
+ // 获取调用表达式的文本
124
+ const callText = callExpr.getText().slice(0, 50);
125
+
126
+ // 查找返回值被赋值给的变量
127
+ let assignedTo: string | undefined;
128
+ let current: ts.Node = callExpr;
129
+
130
+ while (current.parent) {
131
+ const parent = current.parent;
132
+
133
+ // 直接赋值: const x = call()
134
+ if (ts.isVariableDeclaration(parent) && ts.isIdentifier(parent.name)) {
135
+ assignedTo = parent.name.text;
136
+ break;
137
+ }
138
+
139
+ // 数组解构: const [a, b] = await Promise.all([call()])
140
+ if (ts.isArrayLiteralExpression(parent)) {
141
+ const elements = parent.elements;
142
+ const idx = elements.indexOf(current as ts.Expression);
143
+ if (idx >= 0) {
144
+ // parent is ArrayLiteralExpression, go up to find Promise.all CallExpression
145
+ const arrayLiteralParent = parent.parent;
146
+ if (!ts.isCallExpression(arrayLiteralParent)) continue;
147
+
148
+ // Check if it's Promise.all - handle both Promise.all and Promise.all<any>
149
+ let isPromiseAll = false;
150
+ const callExpr = arrayLiteralParent.expression;
151
+ if (ts.isIdentifier(callExpr) && callExpr.text === 'Promise.all') {
152
+ isPromiseAll = true;
153
+ } else if (ts.isPropertyAccessExpression(callExpr)) {
154
+ // Handle Promise.all<any> where expression is PropertyAccessExpression
155
+ const propExpr = callExpr.expression;
156
+ const propName = callExpr.name.text;
157
+ if (ts.isIdentifier(propExpr) && propExpr.text === 'Promise' && propName === 'all') {
158
+ isPromiseAll = true;
159
+ }
160
+ }
161
+ if (!isPromiseAll) continue;
162
+
163
+ // Now go up from Promise.all CallExpression to find AwaitExpression
164
+ const promiseAllParent = arrayLiteralParent.parent;
165
+ if (!ts.isAwaitExpression(promiseAllParent)) continue;
166
+
167
+ // Finally find VariableDeclaration
168
+ const varDeclParent = promiseAllParent.parent;
169
+ if (!ts.isVariableDeclaration(varDeclParent)) continue;
170
+
171
+ // Check if name is array binding pattern (const [a, b] = ...)
172
+ if (!ts.isArrayBindingPattern(varDeclParent.name)) continue;
173
+
174
+ const bindingPattern = varDeclParent.name;
175
+ const bindingElements = bindingPattern.elements;
176
+
177
+ if (bindingElements[idx] && !ts.isOmittedExpression(bindingElements[idx])) {
178
+ const bindingElement = bindingElements[idx] as ts.BindingElement;
179
+ if (ts.isIdentifier(bindingElement.name)) {
180
+ assignedTo = bindingElement.name.text;
181
+ break;
182
+ }
183
+ }
184
+ }
185
+ }
186
+
187
+ current = parent;
188
+ }
189
+
190
+ // 查找后续的属性访问
191
+ const propertyAccesses = assignedTo
192
+ ? this.findPropertyAccesses(sourceFile, assignedTo, callExpr)
193
+ : [];
194
+
195
+ const analysis: CallSiteAnalysis = {
196
+ functionName: functionName, // Use the parameter
197
+ file: sourceFile.fileName,
198
+ line,
199
+ callExpression: callText,
200
+ returnedTo: assignedTo,
201
+ codeContext,
202
+ propertyAccesses,
203
+ };
204
+
205
+ return analysis;
206
+ }
207
+
208
+ /**
209
+ * 查找变量被访问的属性链
210
+ */
211
+ private findPropertyAccesses(
212
+ sourceFile: ts.SourceFile,
213
+ variableName: string,
214
+ afterNode: ts.Node
215
+ ): PropertyAccess[] {
216
+ const accesses: PropertyAccess[] = [];
217
+
218
+ // 获取源代码文本
219
+ const sourceText = sourceFile.getFullText();
220
+ const lineStarts = sourceFile.getLineStarts();
221
+
222
+ // 查找所有对 variableName 的属性访问
223
+ const visit = (node: ts.Node) => {
224
+ // 属性访问: variable.xxx.yyy
225
+ if (ts.isPropertyAccessExpression(node)) {
226
+ const expr = node.expression;
227
+ const propLine = ts.getLineAndCharacterOfPosition(sourceFile, node.getStart()).line + 1;
228
+
229
+ // 获取该行的完整代码
230
+ const lineStart = lineStarts[propLine - 1];
231
+ const lineEnd = lineStarts[propLine] || sourceText.length;
232
+ const lineContext = sourceText.slice(lineStart, lineEnd).trim();
233
+
234
+ // 检查是否是标识符
235
+ if (ts.isIdentifier(expr)) {
236
+ if (expr.text === variableName) {
237
+ // 构建属性访问链
238
+ const chain = this.buildPropertyChain(node);
239
+ accesses.push({
240
+ file: sourceFile.fileName,
241
+ line: propLine,
242
+ variableName,
243
+ accessChain: chain,
244
+ fullExpression: node.getText().slice(0, 80),
245
+ codeContext: lineContext,
246
+ });
247
+ }
248
+ }
249
+
250
+ // 检查链式访问: a.b.c 中的 a
251
+ if (ts.isPropertyAccessExpression(expr)) {
252
+ const baseExpr = this.getBaseIdentifier(expr);
253
+ if (baseExpr && baseExpr.text === variableName) {
254
+ const chain = this.buildPropertyChain(node);
255
+ accesses.push({
256
+ file: sourceFile.fileName,
257
+ line: propLine,
258
+ variableName,
259
+ accessChain: chain,
260
+ fullExpression: node.getText().slice(0, 80),
261
+ codeContext: lineContext,
262
+ });
263
+ }
264
+ }
265
+ }
266
+
267
+ ts.forEachChild(node, visit);
268
+ };
269
+
270
+ visit(sourceFile);
271
+
272
+ return accesses.filter(a => a.line > ts.getLineAndCharacterOfPosition(sourceFile, afterNode.getStart()).line);
273
+ }
274
+
275
+ /**
276
+ * 获取属性访问的基标识符
277
+ */
278
+ private getBaseIdentifier(node: ts.PropertyAccessExpression): ts.Identifier | null {
279
+ let current: ts.Node = node;
280
+ while (ts.isPropertyAccessExpression(current)) {
281
+ current = current.expression;
282
+ }
283
+ return ts.isIdentifier(current) ? current : null;
284
+ }
285
+
286
+ /**
287
+ * 构建属性访问链
288
+ */
289
+ private buildPropertyChain(node: ts.PropertyAccessExpression): string[] {
290
+ const chain: string[] = [];
291
+ let current: ts.Node = node;
292
+
293
+ while (ts.isPropertyAccessExpression(current)) {
294
+ chain.unshift(current.name.text);
295
+ current = current.expression;
296
+ }
297
+
298
+ return chain;
299
+ }
300
+
301
+ /**
302
+ * 生成报告
303
+ */
304
+ generateReport(analyses: CallSiteAnalysis[]): string {
305
+ const lines: string[] = [];
306
+
307
+ lines.push('═══════════════════════════════════════════════════════════════');
308
+ lines.push(' PROPERTY ACCESS ANALYSIS ');
309
+ lines.push('═══════════════════════════════════════════════════════════════');
310
+ lines.push('');
311
+
312
+ if (analyses.length === 0) {
313
+ lines.push('No call sites found.');
314
+ return lines.join('\n');
315
+ }
316
+
317
+ for (const analysis of analyses) {
318
+ lines.push(`📍 ${path.basename(analysis.file)}:${analysis.line}`);
319
+ lines.push(` Call: ${analysis.callExpression}`);
320
+ if (analysis.returnedTo) {
321
+ lines.push(` → Returns to: ${analysis.returnedTo}`);
322
+ }
323
+
324
+ if (analysis.propertyAccesses.length > 0) {
325
+ lines.push(' Properties accessed:');
326
+ for (const access of analysis.propertyAccesses) {
327
+ lines.push(` • ${access.accessChain.join('.')}`);
328
+ lines.push(` Line ${access.line}: ${access.fullExpression}`);
329
+ }
330
+ }
331
+ lines.push('');
332
+ }
333
+
334
+ // 汇总
335
+ const allProperties = analyses.flatMap(a => a.propertyAccesses.map(p => p.accessChain.join('.')));
336
+ const propertyCounts = new Map<string, number>();
337
+ for (const prop of allProperties) {
338
+ propertyCounts.set(prop, (propertyCounts.get(prop) || 0) + 1);
339
+ }
340
+
341
+ lines.push('─── Property Access Summary ────────────────────────────────');
342
+ const sorted = [...propertyCounts.entries()].sort((a, b) => b[1] - a[1]);
343
+ for (const [prop, count] of sorted.slice(0, 10)) {
344
+ lines.push(` ${prop}: ${count} occurrence(s)`);
345
+ }
346
+
347
+ return lines.join('\n');
348
+ }
349
+ }