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,521 @@
1
+ /**
2
+ * ImpactTracer - 追踪改动的传播路径
3
+ *
4
+ * 基于符号级分析,构建完整的依赖图和影响链
5
+ */
6
+
7
+ import {
8
+ Project,
9
+ Node,
10
+ SyntaxKind,
11
+ SourceFile,
12
+ CallExpression,
13
+ PropertyAccessExpression,
14
+ VariableDeclaration,
15
+ FunctionDeclaration,
16
+ ClassDeclaration,
17
+ InterfaceDeclaration,
18
+ TypeAliasDeclaration,
19
+ Decorator,
20
+ } from 'ts-morph';
21
+ import * as path from 'path';
22
+ import * as fs from 'fs';
23
+ import type { AnalyzerConfig } from '../types.js';
24
+ import { SymbolAnalyzer, SymbolInfo, ReferenceInfo } from './SymbolAnalyzer.js';
25
+ import { PropagationTracker, PropagationNode } from './PropagationTracker.js';
26
+
27
+ export interface ImpactScope {
28
+ changedFile: string;
29
+ changedSymbol?: string;
30
+ changeType: 'modify' | 'delete' | 'rename' | 'add';
31
+ timestamp: string;
32
+
33
+ // 核心信息
34
+ symbolInfo: SymbolInfo | null;
35
+
36
+ // 影响统计
37
+ stats: {
38
+ totalAffectedFiles: number;
39
+ directReferences: number;
40
+ indirectReferences: number;
41
+ callSites: number;
42
+ typeReferences: number;
43
+ impactScore: number;
44
+ riskLevel: 'low' | 'medium' | 'high' | 'critical';
45
+ };
46
+
47
+ // 受影响的文件
48
+ affectedFiles: AffectedFile[];
49
+
50
+ // 传播路径
51
+ propagationPaths: PropagationPath[];
52
+
53
+ // 按类型分类
54
+ categorized: {
55
+ definitions: ReferenceInfo[];
56
+ calls: ReferenceInfo[];
57
+ types: ReferenceInfo[];
58
+ exports: ReferenceInfo[];
59
+ extends: ReferenceInfo[];
60
+ implements: ReferenceInfo[];
61
+ properties: ReferenceInfo[];
62
+ };
63
+
64
+ // 建议
65
+ recommendations: string[];
66
+ }
67
+
68
+ export interface AffectedFile {
69
+ file: string;
70
+ line: number;
71
+ references: ReferenceInfo[];
72
+ impactFactors: ImpactFactor[];
73
+ category: string;
74
+ }
75
+
76
+ export interface ImpactFactor {
77
+ type: string;
78
+ weight: number;
79
+ description: string;
80
+ }
81
+
82
+ export interface PropagationPath {
83
+ from: string;
84
+ to: string;
85
+ path: string[];
86
+ type: string;
87
+ }
88
+
89
+ export class ImpactTracer {
90
+ private analyzer: SymbolAnalyzer;
91
+ private propagationTracker: PropagationTracker;
92
+ private projectRoot: string;
93
+ private config: AnalyzerConfig;
94
+
95
+ constructor(projectRoot: string, tsConfigPath: string, config: AnalyzerConfig) {
96
+ this.projectRoot = projectRoot;
97
+ this.config = config;
98
+ this.analyzer = new SymbolAnalyzer(projectRoot, tsConfigPath);
99
+ this.propagationTracker = new PropagationTracker(projectRoot);
100
+ }
101
+
102
+ /**
103
+ * 初始化
104
+ */
105
+ async initialize(): Promise<void> {
106
+ this.analyzer.discoverSourceFiles(this.config.includeTests);
107
+ }
108
+
109
+ /**
110
+ * 追踪改动影响
111
+ */
112
+ async traceImpact(
113
+ file: string,
114
+ symbol?: string,
115
+ changeType: 'modify' | 'delete' | 'rename' | 'add' = 'modify'
116
+ ): Promise<ImpactScope> {
117
+ // 规范化路径
118
+ const normalizedFile = path.resolve(file);
119
+
120
+ // 检查文件是否存在
121
+ if (!fs.existsSync(normalizedFile)) {
122
+ throw new Error(`文件不存在: ${normalizedFile}`);
123
+ }
124
+
125
+ // 查找符号
126
+ let symbolInfo: SymbolInfo | null = null;
127
+ let targetSymbol = symbol;
128
+
129
+ if (symbol) {
130
+ symbolInfo = this.analyzer.findSymbol(symbol, normalizedFile);
131
+ } else {
132
+ // 尝试从文件名推断主要符号
133
+ symbolInfo = this.inferMainSymbol(normalizedFile);
134
+ if (symbolInfo) {
135
+ targetSymbol = symbolInfo.name;
136
+ }
137
+ }
138
+
139
+ // 分析影响 - 如果有符号名,进行全局搜索;否则在文件内搜索
140
+ const analysis = this.analyzer.analyzeImpact(
141
+ targetSymbol || '',
142
+ changeType === 'add' ? 'modify' : changeType,
143
+ targetSymbol ? undefined : normalizedFile // 有符号名时全局搜索
144
+ );
145
+
146
+ // 构建受影响文件列表
147
+ const affectedFiles = this.categorizeAffectedFiles(analysis.references);
148
+
149
+ // 生成传播路径
150
+ let propagationPaths = this.buildPropagationPaths(analysis);
151
+
152
+ // 使用PropagationTracker获取更详细的传播路径
153
+ // 包括:非函数符号、找不到定义符号的嵌套属性等
154
+ if (symbolInfo) {
155
+ if (symbolInfo.kind !== 'function' && symbolInfo.kind !== 'method') {
156
+ const propNodes = this.propagationTracker.trace(targetSymbol!, normalizedFile);
157
+ if (propNodes.length > 0) {
158
+ // 转换传播节点为PropagationPath格式
159
+ const additionalPaths = this.convertPropagationNodesToPaths(propNodes, symbolInfo.kind);
160
+ propagationPaths = [...propagationPaths, ...additionalPaths];
161
+ }
162
+ }
163
+ } else {
164
+ // symbolInfo 为 null,说明不是顶级符号,可能是嵌套属性
165
+ // 强制调用 PropagationTracker 尝试追踪
166
+ const propNodes = this.propagationTracker.trace(targetSymbol!, normalizedFile);
167
+ if (propNodes.length > 0) {
168
+ const additionalPaths = this.convertPropagationNodesToPaths(propNodes, 'nested');
169
+ propagationPaths = [...propagationPaths, ...additionalPaths];
170
+ }
171
+ }
172
+
173
+ // 分类引用
174
+ const categorized = {
175
+ definitions: analysis.references.filter(r => r.referenceType === 'definition'),
176
+ calls: analysis.references.filter(r => r.referenceType === 'call'),
177
+ types: analysis.references.filter(r => r.referenceType === 'type'),
178
+ exports: analysis.references.filter(r => r.referenceType === 'export'),
179
+ extends: analysis.references.filter(r => r.referenceType === 'extend'),
180
+ implements: analysis.references.filter(r => r.referenceType === 'implement'),
181
+ properties: analysis.references.filter(r => r.referenceType === 'property'),
182
+ };
183
+
184
+ // 生成建议
185
+ const recommendations = this.generateRecommendations(symbolInfo, analysis, changeType);
186
+
187
+ return {
188
+ changedFile: normalizedFile,
189
+ changedSymbol: targetSymbol,
190
+ changeType,
191
+ timestamp: new Date().toISOString(),
192
+ symbolInfo,
193
+ stats: {
194
+ totalAffectedFiles: affectedFiles.length,
195
+ directReferences: analysis.references.filter(r => r.impactLevel === 0).length,
196
+ indirectReferences: analysis.references.filter(r => r.impactLevel > 0).length,
197
+ callSites: analysis.callGraph.size,
198
+ typeReferences: analysis.typeDependencies.length,
199
+ impactScore: analysis.impactScore,
200
+ riskLevel: analysis.riskLevel,
201
+ },
202
+ affectedFiles,
203
+ propagationPaths,
204
+ categorized,
205
+ recommendations,
206
+ };
207
+ }
208
+
209
+ /**
210
+ * 从文件名推断主要符号
211
+ */
212
+ private inferMainSymbol(filePath: string): SymbolInfo | null {
213
+ // 提取文件名作为符号名
214
+ const fileName = path.basename(filePath, path.extname(filePath));
215
+
216
+ // 常见模式:index.tsx -> 使用目录名
217
+ if (fileName === 'index') {
218
+ const dirName = path.basename(path.dirname(filePath));
219
+ return this.analyzer.findSymbol(dirName, filePath) ||
220
+ this.analyzer.findMainExport(filePath);
221
+ }
222
+
223
+ // 尝试查找同名的导出符号(必须是导出的)
224
+ const symbol = this.analyzer.findSymbol(fileName, filePath);
225
+ if (symbol && symbol.exports.length > 0) return symbol;
226
+
227
+ // 如果没找到导出的符号,返回文件的主要导出
228
+ return this.analyzer.findMainExport(filePath);
229
+ }
230
+
231
+ /**
232
+ * 分类受影响的文件
233
+ */
234
+ private categorizeAffectedFiles(references: ReferenceInfo[]): AffectedFile[] {
235
+ const fileMap = new Map<string, AffectedFile>();
236
+
237
+ for (const ref of references) {
238
+ const file = ref.location.file;
239
+ if (!fileMap.has(file)) {
240
+ fileMap.set(file, {
241
+ file,
242
+ line: ref.location.line,
243
+ references: [],
244
+ impactFactors: [],
245
+ category: this.categorizeFile(file),
246
+ });
247
+ }
248
+ fileMap.get(file)!.references.push(ref);
249
+
250
+ // 添加影响因子
251
+ const factors = this.computeImpactFactors(ref);
252
+ fileMap.get(file)!.impactFactors.push(...factors);
253
+ }
254
+
255
+ return Array.from(fileMap.values()).sort((a, b) => {
256
+ // 按影响因子权重排序
257
+ const aWeight = a.impactFactors.reduce((s, f) => s + f.weight, 0);
258
+ const bWeight = b.impactFactors.reduce((s, f) => s + f.weight, 0);
259
+ return bWeight - aWeight;
260
+ });
261
+ }
262
+
263
+ /**
264
+ * 计算影响因子
265
+ */
266
+ private computeImpactFactors(ref: ReferenceInfo): ImpactFactor[] {
267
+ const factors: ImpactFactor[] = [];
268
+
269
+ switch (ref.referenceType) {
270
+ case 'call':
271
+ factors.push({
272
+ type: 'function-call',
273
+ weight: 10,
274
+ description: `调用了 ${ref.symbol.name}()`,
275
+ });
276
+ break;
277
+ case 'type':
278
+ factors.push({
279
+ type: 'type-reference',
280
+ weight: 8,
281
+ description: `使用了类型 ${ref.symbol.name}`,
282
+ });
283
+ break;
284
+ case 'extend':
285
+ factors.push({
286
+ type: 'inheritance',
287
+ weight: 15,
288
+ description: `继承了 ${ref.symbol.name}`,
289
+ });
290
+ break;
291
+ case 'implement':
292
+ factors.push({
293
+ type: 'interface',
294
+ weight: 15,
295
+ description: `实现了接口 ${ref.symbol.name}`,
296
+ });
297
+ break;
298
+ case 'property':
299
+ factors.push({
300
+ type: 'property-access',
301
+ weight: 5,
302
+ description: `访问了属性 ${ref.symbol.name}`,
303
+ });
304
+ break;
305
+ case 'export':
306
+ factors.push({
307
+ type: 'export',
308
+ weight: 20,
309
+ description: `重新导出了 ${ref.symbol.name}`,
310
+ });
311
+ break;
312
+ }
313
+
314
+ // 文件类型因子
315
+ if (ref.location.file.includes('/api/')) {
316
+ factors.push({
317
+ type: 'api-layer',
318
+ weight: 12,
319
+ description: 'API 层文件,影响范围大',
320
+ });
321
+ }
322
+ if (ref.location.file.includes('/components/')) {
323
+ factors.push({
324
+ type: 'component',
325
+ weight: 8,
326
+ description: 'UI 组件,可能影响多个页面',
327
+ });
328
+ }
329
+ if (ref.location.file.includes('/pages/') || ref.location.file.includes('/views/')) {
330
+ factors.push({
331
+ type: 'page',
332
+ weight: 10,
333
+ description: '页面文件,直接影响用户体验',
334
+ });
335
+ }
336
+ if (ref.location.file.includes('/hooks/')) {
337
+ factors.push({
338
+ type: 'hook',
339
+ weight: 15,
340
+ description: '共享 Hook,影响多个组件',
341
+ });
342
+ }
343
+ if (ref.location.file.includes('/utils/')) {
344
+ factors.push({
345
+ type: 'utility',
346
+ weight: 18,
347
+ description: '工具函数,被广泛引用',
348
+ });
349
+ }
350
+ if (ref.location.file.includes('/store/') || ref.location.file.includes('/redux') || ref.location.file.includes('/mobx')) {
351
+ factors.push({
352
+ type: 'state',
353
+ weight: 25,
354
+ description: '状态管理层,影响整个应用',
355
+ });
356
+ }
357
+ if (ref.location.file.includes('/context') || ref.location.file.includes('/Context')) {
358
+ factors.push({
359
+ type: 'context',
360
+ weight: 22,
361
+ description: 'Context,可能影响多个组件',
362
+ });
363
+ }
364
+
365
+ return factors;
366
+ }
367
+
368
+ /**
369
+ * 分类文件
370
+ */
371
+ private categorizeFile(filePath: string): string {
372
+ if (filePath.includes('/api/')) return 'API Layer';
373
+ if (filePath.includes('/components/')) return 'Component';
374
+ if (filePath.includes('/pages/') || filePath.includes('/views/')) return 'Page';
375
+ if (filePath.includes('/hooks/')) return 'Hook';
376
+ if (filePath.includes('/utils/')) return 'Utility';
377
+ if (filePath.includes('/store/') || filePath.includes('/redux') || filePath.includes('/mobx')) return 'State Management';
378
+ if (filePath.includes('/context') || filePath.includes('/Context')) return 'Context';
379
+ if (filePath.includes('/types/')) return 'Type Definition';
380
+ if (filePath.includes('/services/')) return 'Service';
381
+ if (filePath.includes('/models/')) return 'Model';
382
+ if (filePath.includes('/middleware/')) return 'Middleware';
383
+ return 'Other';
384
+ }
385
+
386
+ /**
387
+ * 将传播节点转换为传播路径
388
+ */
389
+ private convertPropagationNodesToPaths(
390
+ nodes: PropagationNode[],
391
+ symbolKind: string
392
+ ): PropagationPath[] {
393
+ const paths: PropagationPath[] = [];
394
+
395
+ // 添加根节点作为路径
396
+ for (const node of nodes) {
397
+ paths.push({
398
+ from: node.symbol,
399
+ to: `${node.file}:${node.line}`,
400
+ path: [node.symbol],
401
+ type: node.type || symbolKind || 'propagate',
402
+ });
403
+ // 递归处理子节点
404
+ const traverseChildren = (children: PropagationNode[], parentSymbol: string) => {
405
+ for (const child of children) {
406
+ paths.push({
407
+ from: parentSymbol,
408
+ to: `${child.file}:${child.line}`,
409
+ path: [parentSymbol, child.symbol],
410
+ type: child.type || symbolKind || 'propagate',
411
+ });
412
+ if (child.children.length > 0) {
413
+ traverseChildren(child.children, child.symbol);
414
+ }
415
+ }
416
+ };
417
+ if (node.children.length > 0) {
418
+ traverseChildren(node.children, node.symbol);
419
+ }
420
+ }
421
+
422
+ return paths;
423
+ }
424
+
425
+ /**
426
+ * 构建传播路径
427
+ */
428
+ private buildPropagationPaths(analysis: ReturnType<SymbolAnalyzer['analyzeImpact']>): PropagationPath[] {
429
+ const paths: PropagationPath[] = [];
430
+
431
+ // 从调用图构建路径
432
+ for (const [funcName, refs] of analysis.callGraph.entries()) {
433
+ for (const ref of refs) {
434
+ paths.push({
435
+ from: `${analysis.symbol?.file}:${analysis.symbol?.name}`,
436
+ to: `${ref.location.file}:${ref.location.line}`,
437
+ path: [analysis.symbol?.name || '', funcName],
438
+ type: 'call',
439
+ });
440
+ }
441
+ }
442
+
443
+ // 从类型依赖构建路径
444
+ for (const ref of analysis.typeDependencies) {
445
+ paths.push({
446
+ from: `${analysis.symbol?.file}:${analysis.symbol?.name}`,
447
+ to: `${ref.location.file}:${ref.location.line}`,
448
+ path: [analysis.symbol?.name || '', ref.symbol.name],
449
+ type: 'type',
450
+ });
451
+ }
452
+
453
+ return paths;
454
+ }
455
+
456
+ /**
457
+ * 生成建议
458
+ */
459
+ private generateRecommendations(
460
+ symbol: SymbolInfo | null,
461
+ analysis: ReturnType<SymbolAnalyzer['analyzeImpact']>,
462
+ changeType: string
463
+ ): string[] {
464
+ const recs: string[] = [];
465
+
466
+ // 基于风险等级
467
+ if (analysis.riskLevel === 'critical') {
468
+ recs.push('🚨 极高风险:此改动影响范围极广,建议仔细评估');
469
+ } else if (analysis.riskLevel === 'high') {
470
+ recs.push('⚠️ 高风险:建议进行全面的回归测试');
471
+ }
472
+
473
+ // 基于改动类型
474
+ if (changeType === 'delete') {
475
+ recs.push('🗑️ 删除操作影响最大,确保没有遗漏的引用');
476
+ recs.push('💡 建议:先标记为 @deprecated,逐步废弃后再删除');
477
+ }
478
+
479
+ if (changeType === 'rename') {
480
+ recs.push('✏️ 重命名操作需要同步更新所有引用');
481
+ recs.push('💡 建议:使用 IDE 的重构功能自动更新');
482
+ }
483
+
484
+ // 基于影响分数
485
+ if (analysis.impactScore > 100) {
486
+ recs.push('📊 影响分数较高,建议分阶段发布');
487
+ }
488
+
489
+ // 基于符号类型
490
+ if (symbol?.kind === 'interface') {
491
+ recs.push('📝 接口改动风险较高,确保向后兼容');
492
+ recs.push('💡 建议:新增方法时提供默认实现');
493
+ }
494
+
495
+ if (symbol?.kind === 'class') {
496
+ recs.push('📝 类改动风险较高,注意继承层次');
497
+ recs.push('💡 建议:检查所有子类是否受影响');
498
+ }
499
+
500
+ if (symbol?.kind === 'function') {
501
+ recs.push('📝 函数改动,注意参数兼容性和返回值');
502
+ }
503
+
504
+ // 基于调用点数量
505
+ if (analysis.callGraph.size > 10) {
506
+ recs.push(`📞 此函数被 ${analysis.callGraph.size} 个地方调用,影响面广`);
507
+ }
508
+
509
+ // 基于类型引用数量
510
+ if (analysis.typeDependencies.length > 5) {
511
+ recs.push(`🔗 此类型被 ${analysis.typeDependencies.length} 个地方引用`);
512
+ }
513
+
514
+ // 默认建议
515
+ if (recs.length === 0) {
516
+ recs.push('✅ 影响范围较小,可正常发布');
517
+ }
518
+
519
+ return recs;
520
+ }
521
+ }