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,502 @@
1
+ /**
2
+ * PropagationTracker - 非函数符号的传播追踪器
3
+ *
4
+ * 追踪常量、类型、对象等非函数符号如何传播到下游
5
+ */
6
+ import { Project, Node, SyntaxKind, } from 'ts-morph';
7
+ export class PropagationTracker {
8
+ project;
9
+ projectRoot;
10
+ constructor(projectRoot) {
11
+ this.projectRoot = projectRoot;
12
+ this.project = new Project({
13
+ tsConfigFilePath: `${projectRoot}/tsconfig.json`,
14
+ skipAddingFilesFromTsConfig: true,
15
+ });
16
+ // 添加源文件
17
+ this.project.addSourceFilesAtPaths(`${projectRoot}/**/*.ts`);
18
+ }
19
+ /**
20
+ * 追踪符号的传播路径
21
+ */
22
+ trace(symbolName, filePath, maxDepth = 5) {
23
+ const roots = [];
24
+ const visited = new Set();
25
+ // 1. 首先尝试找到符号定义(顶级声明)
26
+ let definition = this.findDefinition(symbolName, filePath);
27
+ // 2. 如果没找到,尝试作为嵌套属性查找(如 config.api.baseUrl 中的 baseUrl)
28
+ if (!definition) {
29
+ const nestedResult = this.findNestedProperty(symbolName, filePath);
30
+ if (nestedResult) {
31
+ // 追踪父对象的传播
32
+ this.buildPropagationTree(nestedResult.parentObj, filePath, roots, visited, 0, maxDepth, 'object');
33
+ // 添加嵌套属性的引用信息
34
+ for (const ref of nestedResult.references) {
35
+ const node = {
36
+ symbol: `${nestedResult.parentObj}.${symbolName}`,
37
+ file: ref.file,
38
+ line: ref.line,
39
+ type: 'nested',
40
+ children: [],
41
+ context: ref.context,
42
+ };
43
+ roots.push(node);
44
+ }
45
+ return roots;
46
+ }
47
+ }
48
+ if (!definition)
49
+ return [];
50
+ // 构建传播树
51
+ this.buildPropagationTree(symbolName, filePath, roots, visited, 0, maxDepth, definition.type);
52
+ return roots;
53
+ }
54
+ /**
55
+ * 查找嵌套属性(如在对象字面量中定义的属性)
56
+ */
57
+ findNestedProperty(propertyName, filePath) {
58
+ const sourceFile = this.project.getSourceFile(filePath);
59
+ if (!sourceFile)
60
+ return null;
61
+ // 查找所有对象字面量
62
+ const references = [];
63
+ for (const sf of this.project.getSourceFiles()) {
64
+ sf.forEachDescendant(node => {
65
+ // 查找 PropertyAssignment(对象字面量中的属性)
66
+ if (Node.isPropertyAssignment(node)) {
67
+ const name = node.getName();
68
+ if (name === propertyName) {
69
+ // 找到嵌套属性,查找其父对象
70
+ let parentObj = this.findParentObjectName(node);
71
+ if (parentObj) {
72
+ const line = sf.getLineAndColumnAtPos(node.getStart()).line;
73
+ const lineText = sf.getFullText().split('\n')[line - 1]?.trim() ?? '';
74
+ references.push({
75
+ file: sf.getFilePath(),
76
+ line,
77
+ context: lineText.slice(0, 80),
78
+ parentObj,
79
+ });
80
+ }
81
+ }
82
+ }
83
+ // 查找 PropertyAccessExpression(属性访问,如 obj.prop)
84
+ if (Node.isPropertyAccessExpression(node)) {
85
+ const accessedName = node.getName();
86
+ if (accessedName === propertyName) {
87
+ // 检查是否是嵌套属性访问(obj.nested.prop)
88
+ const expr = node.getExpression();
89
+ if (Node.isPropertyAccessExpression(expr)) {
90
+ // 这是嵌套访问,尝试获取完整的路径
91
+ const fullPath = node.getText();
92
+ let current = node;
93
+ let topLevelObj = '';
94
+ while (current) {
95
+ if (Node.isIdentifier(current)) {
96
+ topLevelObj = current.getText();
97
+ break;
98
+ }
99
+ if (Node.isPropertyAccessExpression(current)) {
100
+ current = current.getExpression();
101
+ }
102
+ else {
103
+ break;
104
+ }
105
+ }
106
+ if (topLevelObj) {
107
+ const line = sf.getLineAndColumnAtPos(node.getStart()).line;
108
+ const lineText = sf.getFullText().split('\n')[line - 1]?.trim() ?? '';
109
+ references.push({
110
+ file: sf.getFilePath(),
111
+ line,
112
+ context: lineText.slice(0, 80),
113
+ parentObj: topLevelObj,
114
+ });
115
+ }
116
+ }
117
+ }
118
+ }
119
+ });
120
+ }
121
+ if (references.length === 0)
122
+ return null;
123
+ // 返回第一个找到的父对象名
124
+ return {
125
+ definition: { node: references[0], type: 'object' },
126
+ parentObj: references[0].parentObj,
127
+ references: references.map(r => ({ file: r.file, line: r.line, context: r.context })),
128
+ };
129
+ }
130
+ /**
131
+ * 查找对象字面量中属性的父对象名
132
+ */
133
+ findParentObjectName(node) {
134
+ let current = node.getParent();
135
+ while (current) {
136
+ if (Node.isVariableDeclaration(current)) {
137
+ return current.getName();
138
+ }
139
+ if (Node.isPropertyAssignment(current)) {
140
+ // 继续向上找
141
+ }
142
+ if (Node.isObjectLiteralExpression(current)) {
143
+ // 找到了对象字面量,继续向上找变量声明
144
+ }
145
+ current = current.getParent();
146
+ }
147
+ return null;
148
+ }
149
+ /**
150
+ * 查找符号定义
151
+ */
152
+ findDefinition(symbolName, filePath) {
153
+ const sourceFile = this.project.getSourceFile(filePath);
154
+ if (!sourceFile)
155
+ return null;
156
+ let result = null;
157
+ sourceFile.forEachDescendant(node => {
158
+ // 查找 const/let/var 声明
159
+ if (Node.isVariableDeclaration(node)) {
160
+ const name = node.getName();
161
+ if (name === symbolName) {
162
+ const initializer = node.getInitializer();
163
+ result = {
164
+ node,
165
+ type: 'constant',
166
+ value: initializer?.getText() ?? undefined,
167
+ };
168
+ }
169
+ }
170
+ // 查找 type alias
171
+ if (Node.isTypeAliasDeclaration(node)) {
172
+ const name = node.getName();
173
+ if (name === symbolName) {
174
+ result = { node, type: 'type' };
175
+ }
176
+ }
177
+ // 查找 interface
178
+ if (Node.isInterfaceDeclaration(node)) {
179
+ const name = node.getName();
180
+ if (name === symbolName) {
181
+ result = { node, type: 'type' };
182
+ }
183
+ }
184
+ // 查找 class
185
+ if (Node.isClassDeclaration(node)) {
186
+ const name = node.getName();
187
+ if (name === symbolName) {
188
+ result = { node, type: 'object' };
189
+ }
190
+ }
191
+ });
192
+ return result;
193
+ }
194
+ /**
195
+ * 查找符号的所有引用位置
196
+ */
197
+ findReferences(symbolName, filePath) {
198
+ const references = [];
199
+ const sourceFiles = this.project.getSourceFiles();
200
+ for (const sourceFile of sourceFiles) {
201
+ sourceFile.forEachDescendant(node => {
202
+ if (Node.isIdentifier(node)) {
203
+ if (node.getText() === symbolName) {
204
+ // 跳过定义位置
205
+ const parent = node.getParent();
206
+ if (parent && Node.isVariableDeclaration(parent) && parent.getName() === symbolName) {
207
+ return;
208
+ }
209
+ const file = sourceFile.getFilePath();
210
+ const line = sourceFile.getLineAndColumnAtPos(node.getStart()).line;
211
+ const grandParent = node.getParent()?.getParent();
212
+ references.push({
213
+ node,
214
+ file,
215
+ line,
216
+ parent: grandParent ?? node.getParent(),
217
+ context: this.getContextCode(node),
218
+ });
219
+ }
220
+ }
221
+ });
222
+ }
223
+ return references;
224
+ }
225
+ /**
226
+ * 获取上下文代码
227
+ */
228
+ getContextCode(node) {
229
+ const sourceFile = node.getSourceFile();
230
+ const start = node.getStart();
231
+ const line = sourceFile.getLineAndColumnAtPos(start).line;
232
+ const lineText = sourceFile.getFullText().split('\n')[line - 1]?.trim() ?? '';
233
+ return lineText.slice(0, 80);
234
+ }
235
+ /**
236
+ * 构建传播树
237
+ */
238
+ buildPropagationTree(symbolName, filePath, roots, visited, depth, maxDepth, symbolType) {
239
+ if (depth > maxDepth)
240
+ return;
241
+ const refs = this.findReferences(symbolName, filePath);
242
+ for (const ref of refs) {
243
+ const key = `${ref.file}:${ref.line}`;
244
+ if (visited.has(key))
245
+ continue;
246
+ visited.add(key);
247
+ const propagationType = this.classifyUsage(ref.node, ref.parent);
248
+ const childNode = {
249
+ symbol: symbolName,
250
+ file: ref.file,
251
+ line: ref.line,
252
+ type: propagationType,
253
+ children: [],
254
+ context: ref.context,
255
+ };
256
+ roots.push(childNode);
257
+ // 如果是变量赋值,追踪赋值的变量
258
+ if (propagationType === 'variable') {
259
+ const assignedVar = this.getAssignedVariable(ref.node, ref.parent);
260
+ if (assignedVar) {
261
+ this.buildPropagationTree(assignedVar, ref.file, childNode.children, visited, depth + 1, maxDepth, 'variable');
262
+ }
263
+ }
264
+ // 如果是常量传播(如在模板字符串中),追踪该常量所在的函数或赋值
265
+ if (propagationType === 'constant' && ref.parent) {
266
+ // 找到包含这个常量的上下文(如模板表达式、函数参数等)
267
+ this.traceConstantContext(ref.node, ref.file, childNode, visited, depth + 1, maxDepth);
268
+ }
269
+ // 如果是函数参数,追踪函数返回值
270
+ if (propagationType === 'functionArg') {
271
+ const funcName = this.getContainingFunction(ref.node);
272
+ if (funcName) {
273
+ childNode.type = 'functionArg';
274
+ // 追踪函数返回值使用
275
+ this.traceFunctionReturnUsage(funcName, ref.file, childNode, visited, depth + 1, maxDepth);
276
+ }
277
+ }
278
+ }
279
+ }
280
+ /**
281
+ * 分类符号的使用方式
282
+ */
283
+ classifyUsage(node, parent) {
284
+ if (Node.isPropertyAccessExpression(parent)) {
285
+ return 'property';
286
+ }
287
+ if (Node.isTemplateExpression(parent)) {
288
+ return 'constant';
289
+ }
290
+ if (Node.isBinaryExpression(parent)) {
291
+ const op = parent.getOperatorToken().getKind();
292
+ if (op === SyntaxKind.EqualsToken) {
293
+ return 'variable';
294
+ }
295
+ return 'constant';
296
+ }
297
+ if (Node.isVariableDeclaration(parent)) {
298
+ return 'variable';
299
+ }
300
+ if (Node.isPropertyAssignment(parent)) {
301
+ return 'property';
302
+ }
303
+ if (Node.isArrayLiteralExpression(parent)) {
304
+ return 'constant';
305
+ }
306
+ if (Node.isCallExpression(parent)) {
307
+ return 'functionArg';
308
+ }
309
+ if (Node.isReturnStatement(parent?.getParent())) {
310
+ return 'return';
311
+ }
312
+ if (Node.isIfStatement(parent)) {
313
+ return 'constant';
314
+ }
315
+ return 'constant';
316
+ }
317
+ /**
318
+ * 追踪常量使用的上下文(如模板字符串、函数调用等)
319
+ */
320
+ traceConstantContext(node, filePath, parentNode, visited, depth, maxDepth) {
321
+ if (depth > maxDepth)
322
+ return;
323
+ const sourceFile = this.project.getSourceFile(filePath);
324
+ if (!sourceFile)
325
+ return;
326
+ // 向上遍历找到包含的函数或赋值语句
327
+ let current = node.getParent();
328
+ while (current) {
329
+ // 如果是函数声明/表达式
330
+ if (Node.isFunctionDeclaration(current) || Node.isArrowFunction(current)) {
331
+ const funcName = this.getContainingFunction(node);
332
+ if (funcName && funcName !== 'arrow') {
333
+ parentNode.children.push({
334
+ symbol: `📥 ${funcName}()`,
335
+ file: filePath,
336
+ line: sourceFile.getLineAndColumnAtPos(current.getStart()).line,
337
+ type: 'functionArg',
338
+ children: [],
339
+ context: `Function using constant`,
340
+ });
341
+ }
342
+ break;
343
+ }
344
+ // 如果是赋值表达式
345
+ if (Node.isBinaryExpression(current)) {
346
+ const op = current.getOperatorToken().getKind();
347
+ if (op === SyntaxKind.EqualsToken) {
348
+ const left = current.getLeft();
349
+ if (Node.isIdentifier(left)) {
350
+ const varName = left.getText();
351
+ const key = `${filePath}:${varName}:assigned`;
352
+ if (!visited.has(key)) {
353
+ visited.add(key);
354
+ parentNode.children.push({
355
+ symbol: `📦 ${varName}`,
356
+ file: filePath,
357
+ line: sourceFile.getLineAndColumnAtPos(current.getStart()).line,
358
+ type: 'variable',
359
+ children: [],
360
+ context: `Assigned from constant`,
361
+ });
362
+ // 递归追踪这个变量
363
+ this.buildPropagationTree(varName, filePath, parentNode.children, visited, depth + 1, maxDepth, 'variable');
364
+ }
365
+ }
366
+ }
367
+ break;
368
+ }
369
+ // 如果是函数调用
370
+ if (Node.isCallExpression(current)) {
371
+ const expr = current.getExpression();
372
+ if (Node.isIdentifier(expr)) {
373
+ const funcName = expr.getText();
374
+ parentNode.children.push({
375
+ symbol: `📥 ${funcName}()`,
376
+ file: filePath,
377
+ line: sourceFile.getLineAndColumnAtPos(current.getStart()).line,
378
+ type: 'functionArg',
379
+ children: [],
380
+ context: `Passed to function`,
381
+ });
382
+ }
383
+ break;
384
+ }
385
+ // 如果是return语句
386
+ if (Node.isReturnStatement(current)) {
387
+ parentNode.children.push({
388
+ symbol: `📤 return`,
389
+ file: filePath,
390
+ line: sourceFile.getLineAndColumnAtPos(current.getStart()).line,
391
+ type: 'return',
392
+ children: [],
393
+ context: `Returned from function`,
394
+ });
395
+ break;
396
+ }
397
+ current = current.getParent();
398
+ }
399
+ }
400
+ /**
401
+ * 获取赋值的变量名
402
+ */
403
+ getAssignedVariable(node, parent) {
404
+ if (Node.isBinaryExpression(parent)) {
405
+ const left = parent.getLeft();
406
+ if (Node.isIdentifier(left)) {
407
+ return left.getText();
408
+ }
409
+ }
410
+ return null;
411
+ }
412
+ /**
413
+ * 获取包含函数名
414
+ */
415
+ getContainingFunction(node) {
416
+ let current = node;
417
+ while (current) {
418
+ if (Node.isFunctionDeclaration(current)) {
419
+ // 检查是否是导出函数
420
+ const parent = current.getParent();
421
+ if (Node.isVariableDeclaration(parent)) {
422
+ return parent.getName();
423
+ }
424
+ if (Node.isExportDeclaration(current.getParent())) {
425
+ return current.getName() ?? 'anonymous';
426
+ }
427
+ return current.getName() ?? 'anonymous';
428
+ }
429
+ if (Node.isArrowFunction(current)) {
430
+ const parent = current.getParent();
431
+ if (Node.isVariableDeclaration(parent)) {
432
+ return parent.getName();
433
+ }
434
+ if (Node.isPropertyAssignment(parent)) {
435
+ return parent.getName() ?? 'arrow';
436
+ }
437
+ return 'arrow';
438
+ }
439
+ if (Node.isMethodDeclaration(current)) {
440
+ return current.getName() ?? 'method';
441
+ }
442
+ current = current.getParent();
443
+ }
444
+ return null;
445
+ }
446
+ /**
447
+ * 追踪函数返回值的用法
448
+ */
449
+ traceFunctionReturnUsage(funcName, filePath, parentNode, visited, depth, maxDepth) {
450
+ if (depth > maxDepth)
451
+ return;
452
+ const sourceFile = this.project.getSourceFile(filePath);
453
+ if (!sourceFile)
454
+ return;
455
+ sourceFile.forEachDescendant(node => {
456
+ if (Node.isCallExpression(node)) {
457
+ const expr = node.getExpression();
458
+ if (Node.isIdentifier(expr) && expr.getText() === funcName) {
459
+ const key = `${filePath}:${node.getStartLineNumber()}:return`;
460
+ if (!visited.has(key)) {
461
+ visited.add(key);
462
+ parentNode.children.push({
463
+ symbol: `${funcName}()`,
464
+ file: filePath,
465
+ line: node.getStartLineNumber(),
466
+ type: 'return',
467
+ children: [],
468
+ context: this.getContextCode(node),
469
+ });
470
+ }
471
+ }
472
+ }
473
+ });
474
+ }
475
+ /**
476
+ * 格式化传播树为文本
477
+ */
478
+ formatAsText(nodes, indent = '') {
479
+ const lines = [];
480
+ for (const node of nodes) {
481
+ const typeIcon = this.getTypeIcon(node.type);
482
+ lines.push(`${indent}${typeIcon} ${node.symbol} → ${node.file}:${node.line}`);
483
+ lines.push(`${indent} 上下文: ${node.context}`);
484
+ if (node.children.length > 0) {
485
+ lines.push(...this.formatAsText(node.children, indent + ' '));
486
+ }
487
+ }
488
+ return lines.join('\n');
489
+ }
490
+ getTypeIcon(type) {
491
+ switch (type) {
492
+ case 'constant': return '📍';
493
+ case 'variable': return '📦';
494
+ case 'type': return '🏷️';
495
+ case 'object': return '📦';
496
+ case 'property': return '🔗';
497
+ case 'functionArg': return '📥';
498
+ case 'return': return '📤';
499
+ default: return '📍';
500
+ }
501
+ }
502
+ }
@@ -0,0 +1,56 @@
1
+ /**
2
+ * PropertyAccessTracker - 属性访问追踪器
3
+ *
4
+ * 追踪函数返回值的属性访问,帮助理解类型传播
5
+ */
6
+ export interface PropertyAccess {
7
+ file: string;
8
+ line: number;
9
+ variableName: string;
10
+ accessChain: string[];
11
+ fullExpression: string;
12
+ codeContext: string;
13
+ }
14
+ export interface CallSiteAnalysis {
15
+ functionName: string;
16
+ file: string;
17
+ line: number;
18
+ callExpression: string;
19
+ returnedTo?: string;
20
+ codeContext: string;
21
+ propertyAccesses: PropertyAccess[];
22
+ }
23
+ export declare class PropertyAccessTracker {
24
+ private program;
25
+ private checker;
26
+ private projectRoot;
27
+ constructor(projectRoot: string, tsConfigPath: string);
28
+ /**
29
+ * 分析函数调用的属性访问链
30
+ */
31
+ analyzeFunctionCalls(functionName: string, inFiles?: string[]): CallSiteAnalysis[];
32
+ /**
33
+ * 分析单个文件
34
+ */
35
+ private analyzeFile;
36
+ /**
37
+ * 分析调用点
38
+ */
39
+ private analyzeCallSite;
40
+ /**
41
+ * 查找变量被访问的属性链
42
+ */
43
+ private findPropertyAccesses;
44
+ /**
45
+ * 获取属性访问的基标识符
46
+ */
47
+ private getBaseIdentifier;
48
+ /**
49
+ * 构建属性访问链
50
+ */
51
+ private buildPropertyChain;
52
+ /**
53
+ * 生成报告
54
+ */
55
+ generateReport(analyses: CallSiteAnalysis[]): string;
56
+ }