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,99 @@
1
+ /**
2
+ * BlastRadiusAnalyzer - 改动影响范围分析器
3
+ *
4
+ * 核心功能:
5
+ * 1. 接收代码改动信息
6
+ * 2. 在依赖图上追溯影响范围
7
+ * 3. 评估风险等级
8
+ * 4. 生成详细的影响报告
9
+ */
10
+ import type { CodeChange, ImpactScope, AnalyzerConfig } from '../types.js';
11
+ export declare class BlastRadiusAnalyzer {
12
+ private config;
13
+ private graph;
14
+ constructor(config: AnalyzerConfig);
15
+ /**
16
+ * 初始化:构建项目依赖图
17
+ */
18
+ initialize(): Promise<void>;
19
+ /**
20
+ * 分析单个改动的爆炸半径
21
+ */
22
+ analyzeChange(change: CodeChange): ImpactScope;
23
+ /**
24
+ * 分析多个改动的综合影响
25
+ */
26
+ analyzeChanges(changes: CodeChange[]): ImpactScope[];
27
+ /**
28
+ * 合并多个 ImpactScope 的综合影响
29
+ */
30
+ mergeImpacts(scopes: ImpactScope[]): ImpactScope;
31
+ /**
32
+ * 找到改动对应的节点
33
+ */
34
+ private findChangedNodes;
35
+ /**
36
+ * 追溯影响范围 (BFS)
37
+ */
38
+ private traceImpact;
39
+ /**
40
+ * 查找连接两节点的边
41
+ */
42
+ private findEdge;
43
+ /**
44
+ * 计算统计信息
45
+ */
46
+ private computeStats;
47
+ /**
48
+ * 按深度分组
49
+ */
50
+ private groupByDepth;
51
+ /**
52
+ * 计算详细的受影响文件
53
+ */
54
+ private computeAffectedFiles;
55
+ /**
56
+ * 评估边类型的影响
57
+ */
58
+ private evaluateEdgeType;
59
+ /**
60
+ * 计算传播路径
61
+ */
62
+ private computePropagationPaths;
63
+ /**
64
+ * 计算风险等级
65
+ */
66
+ private calculateRiskLevel;
67
+ /**
68
+ * 识别高风险影响
69
+ */
70
+ private identifyHighRiskImpacts;
71
+ /**
72
+ * 解释风险
73
+ */
74
+ private explainRisk;
75
+ /**
76
+ * 建议缓解措施
77
+ */
78
+ private suggestMitigation;
79
+ /**
80
+ * 估算破坏性变更数量
81
+ */
82
+ private estimateBreakingChanges;
83
+ /**
84
+ * 生成建议
85
+ */
86
+ private generateRecommendations;
87
+ /**
88
+ * 去重建议
89
+ */
90
+ private deduplicateRecommendations;
91
+ /**
92
+ * 简单 glob 模式匹配
93
+ */
94
+ private matchPattern;
95
+ /**
96
+ * 创建空的影响范围
97
+ */
98
+ private createEmptyScope;
99
+ }
@@ -0,0 +1,510 @@
1
+ /**
2
+ * BlastRadiusAnalyzer - 改动影响范围分析器
3
+ *
4
+ * 核心功能:
5
+ * 1. 接收代码改动信息
6
+ * 2. 在依赖图上追溯影响范围
7
+ * 3. 评估风险等级
8
+ * 4. 生成详细的影响报告
9
+ */
10
+ import { DependencyGraphBuilder } from './DependencyGraph.js';
11
+ export class BlastRadiusAnalyzer {
12
+ config;
13
+ graph = null;
14
+ constructor(config) {
15
+ this.config = config;
16
+ }
17
+ /**
18
+ * 初始化:构建项目依赖图
19
+ */
20
+ async initialize() {
21
+ const builder = new DependencyGraphBuilder(this.config.projectRoot, this.config.tsConfigPath);
22
+ builder.discoverSourceFiles(this.config.includeTests);
23
+ this.graph = builder.build();
24
+ if (this.config.verbose) {
25
+ console.log(`[BlastRadius] Graph built: ${this.graph.nodes.size} nodes, ${this.graph.edges.length} edges`);
26
+ console.log(`[BlastRadius] Entry points: ${this.graph.entryPoints.join(', ')}`);
27
+ }
28
+ }
29
+ /**
30
+ * 分析单个改动的爆炸半径
31
+ */
32
+ analyzeChange(change) {
33
+ if (!this.graph) {
34
+ throw new Error('Analyzer not initialized. Call initialize() first.');
35
+ }
36
+ const startTime = Date.now();
37
+ // 1. 找到改动节点
38
+ const changedNodes = this.findChangedNodes(change);
39
+ if (changedNodes.length === 0) {
40
+ return this.createEmptyScope(change);
41
+ }
42
+ // 2. BFS/DFS 追溯影响范围
43
+ const affectedNodes = this.traceImpact(changedNodes, change.type);
44
+ // 3. 统计信息
45
+ const stats = this.computeStats(affectedNodes, changedNodes);
46
+ // 4. 按层级分组
47
+ const levels = this.groupByDepth(affectedNodes, changedNodes);
48
+ // 5. 详细的受影响文件
49
+ const affectedFiles = this.computeAffectedFiles(affectedNodes, change);
50
+ // 6. 传播路径
51
+ const propagationPaths = this.computePropagationPaths(changedNodes, affectedNodes);
52
+ // 7. 高风险影响
53
+ const highRiskImpacts = this.identifyHighRiskImpacts(affectedNodes, change);
54
+ // 8. 建议
55
+ const recommendations = this.generateRecommendations(affectedNodes, change, stats);
56
+ return {
57
+ changedFile: change.file,
58
+ timestamp: new Date().toISOString(),
59
+ stats,
60
+ levels,
61
+ affectedFiles,
62
+ propagationPaths,
63
+ highRiskImpacts,
64
+ recommendations,
65
+ };
66
+ }
67
+ /**
68
+ * 分析多个改动的综合影响
69
+ */
70
+ analyzeChanges(changes) {
71
+ return changes.map((change) => this.analyzeChange(change));
72
+ }
73
+ /**
74
+ * 合并多个 ImpactScope 的综合影响
75
+ */
76
+ mergeImpacts(scopes) {
77
+ if (scopes.length === 0) {
78
+ throw new Error('No scopes to merge');
79
+ }
80
+ const allAffectedFiles = new Map();
81
+ const allHighRisk = [];
82
+ const allPaths = [];
83
+ for (const scope of scopes) {
84
+ // 合并 affectedFiles
85
+ for (const af of scope.affectedFiles) {
86
+ const existing = allAffectedFiles.get(af.file);
87
+ if (!existing || af.impactLevel < existing.impactLevel) {
88
+ allAffectedFiles.set(af.file, af);
89
+ }
90
+ }
91
+ // 合并高风险
92
+ allHighRisk.push(...scope.highRiskImpacts);
93
+ // 合并路径
94
+ allPaths.push(...scope.propagationPaths);
95
+ }
96
+ // 计算合并后的统计
97
+ const uniqueFiles = new Set(allAffectedFiles.values());
98
+ const stats = {
99
+ totalAffectedFiles: uniqueFiles.size,
100
+ directDependencies: scopes.reduce((s, sc) => s + sc.stats.directDependencies, 0),
101
+ transitiveDependencies: scopes.reduce((s, sc) => s + sc.stats.transitiveDependencies, 0),
102
+ filesWithBreakingChanges: scopes.reduce((s, sc) => s + sc.stats.filesWithBreakingChanges, 0),
103
+ criticalFiles: scopes.reduce((s, sc) => s + sc.stats.criticalFiles, 0),
104
+ estimatedRippleDepth: Math.max(...scopes.map((s) => s.stats.estimatedRippleDepth)),
105
+ };
106
+ return {
107
+ changedFile: 'MULTI',
108
+ timestamp: new Date().toISOString(),
109
+ stats,
110
+ levels: [],
111
+ affectedFiles: Array.from(allAffectedFiles.values()),
112
+ propagationPaths: allPaths,
113
+ highRiskImpacts: allHighRisk,
114
+ recommendations: this.deduplicateRecommendations(scopes.flatMap((s) => s.recommendations)),
115
+ };
116
+ }
117
+ // ─── 私有方法 ───────────────────────────────────────────────────────────────
118
+ /**
119
+ * 找到改动对应的节点
120
+ */
121
+ findChangedNodes(change) {
122
+ const nodes = [];
123
+ // 1. 精确匹配文件路径
124
+ const fileNode = this.graph.nodes.get(change.file);
125
+ if (fileNode) {
126
+ nodes.push(fileNode);
127
+ }
128
+ // 2. 如果有符号名,尝试符号级别匹配
129
+ if (change.symbol) {
130
+ const symbolId = `${change.file}#${change.symbol}`;
131
+ const symbolNode = this.graph.nodes.get(symbolId);
132
+ if (symbolNode) {
133
+ nodes.push(symbolNode);
134
+ }
135
+ }
136
+ // 注意: 不要在这里查找 dependents,traceImpact 会通过 BFS 发现它们
137
+ return nodes;
138
+ }
139
+ /**
140
+ * 追溯影响范围 (BFS)
141
+ */
142
+ traceImpact(startNodes, changeType) {
143
+ const affected = new Map();
144
+ const queue = startNodes.map((n) => ({
145
+ node: n,
146
+ depth: 0,
147
+ path: [n.id],
148
+ edgeTypes: [],
149
+ }));
150
+ const visited = new Set();
151
+ while (queue.length > 0) {
152
+ const current = queue.shift();
153
+ if (visited.has(current.node.id))
154
+ continue;
155
+ visited.add(current.node.id);
156
+ // 记录受影响的节点
157
+ if (!affected.has(current.node.id)) {
158
+ affected.set(current.node.id, {
159
+ node: current.node,
160
+ depth: current.depth,
161
+ path: current.path,
162
+ edgeTypes: current.edgeTypes,
163
+ });
164
+ }
165
+ // 达到最大深度停止
166
+ if (current.depth >= this.config.maxDepth)
167
+ continue;
168
+ // BFS: 查找所有依赖此节点的节点 (即会受影响的节点)
169
+ // 注意: dependents 是反向索引 - 如果 A imports B, 则 B.dependents 包含 A
170
+ // 当我们从 B 遍历到 A 时,边方向是 A -> B, 所以要反向查找边
171
+ for (const dependentId of current.node.dependents) {
172
+ const dependentNode = this.graph.nodes.get(dependentId);
173
+ if (!dependentNode || visited.has(dependentId))
174
+ continue;
175
+ // 查找连接边 (反向: dependent -> current.node)
176
+ const edge = this.findEdge(dependentId, current.node.id);
177
+ if (!edge)
178
+ continue;
179
+ queue.push({
180
+ node: dependentNode,
181
+ depth: current.depth + 1,
182
+ path: [...current.path, dependentId],
183
+ edgeTypes: [...current.edgeTypes, edge.type],
184
+ });
185
+ }
186
+ }
187
+ return affected;
188
+ }
189
+ /**
190
+ * 查找连接两节点的边
191
+ */
192
+ findEdge(from, to) {
193
+ return this.graph.edges.find((e) => e.from === from && e.to === to);
194
+ }
195
+ /**
196
+ * 计算统计信息
197
+ */
198
+ computeStats(affected, changedNodes) {
199
+ const changedFiles = new Set(changedNodes.map((n) => n.file || n.id));
200
+ const criticalPatterns = this.config.criticalPatterns;
201
+ let criticalFiles = 0;
202
+ let directDeps = 0;
203
+ let transitiveDeps = 0;
204
+ for (const { node, depth } of affected.values()) {
205
+ const file = node.file || node.id;
206
+ // 检查是否关键文件
207
+ if (criticalPatterns.some((p) => this.matchPattern(file, p))) {
208
+ criticalFiles++;
209
+ }
210
+ // 区分直接和传递依赖
211
+ if (depth === 1) {
212
+ directDeps++;
213
+ }
214
+ else if (depth > 1) {
215
+ transitiveDeps++;
216
+ }
217
+ }
218
+ return {
219
+ totalAffectedFiles: affected.size,
220
+ directDependencies: directDeps,
221
+ transitiveDependencies: transitiveDeps,
222
+ filesWithBreakingChanges: this.estimateBreakingChanges(affected),
223
+ criticalFiles,
224
+ estimatedRippleDepth: Math.max(...Array.from(affected.values()).map((a) => a.depth), 0),
225
+ };
226
+ }
227
+ /**
228
+ * 按深度分组
229
+ */
230
+ groupByDepth(affected, changedNodes) {
231
+ const byDepth = new Map();
232
+ // 添加改动节点本身 (depth = 0)
233
+ byDepth.set(0, changedNodes);
234
+ // 分组
235
+ for (const { node, depth } of affected.values()) {
236
+ if (!byDepth.has(depth)) {
237
+ byDepth.set(depth, []);
238
+ }
239
+ byDepth.get(depth).push(node);
240
+ }
241
+ const levels = [];
242
+ const depthDescriptions = {
243
+ 0: '直接改动',
244
+ 1: '直接依赖',
245
+ 2: '二级传递依赖',
246
+ 3: '三级传递依赖',
247
+ 4: '深层传递依赖',
248
+ };
249
+ for (const [depth, nodes] of byDepth.entries()) {
250
+ levels.push({
251
+ depth,
252
+ description: depthDescriptions[depth] || `深度 ${depth}`,
253
+ files: nodes.map((n) => n.file || n.id).filter(Boolean),
254
+ nodeCount: nodes.length,
255
+ });
256
+ }
257
+ return levels.sort((a, b) => a.depth - b.depth);
258
+ }
259
+ /**
260
+ * 计算详细的受影响文件
261
+ */
262
+ computeAffectedFiles(affected, change) {
263
+ const affectedFiles = [];
264
+ for (const [nodeId, { node, depth, edgeTypes }] of affected.entries()) {
265
+ if (nodeId === change.file)
266
+ continue; // 跳过改动文件本身
267
+ const file = node.file || nodeId;
268
+ // 计算影响因子
269
+ const impactFactors = [];
270
+ // 1. 边的类型
271
+ for (const edgeType of edgeTypes) {
272
+ const factor = this.evaluateEdgeType(edgeType, depth);
273
+ if (factor) {
274
+ impactFactors.push(factor);
275
+ }
276
+ }
277
+ // 2. 文件类型
278
+ if (file.includes('/components/') || file.includes('/Components/')) {
279
+ impactFactors.push({
280
+ factor: 'ui-component',
281
+ weight: 0.6,
282
+ reason: 'UI 组件变更可能影响多个页面',
283
+ });
284
+ }
285
+ if (file.includes('/api/') || file.includes('/Api/')) {
286
+ impactFactors.push({
287
+ factor: 'api-layer',
288
+ weight: 0.8,
289
+ reason: 'API 层变更影响所有调用方',
290
+ });
291
+ }
292
+ if (file.includes('/hooks/') || file.includes('/utils/')) {
293
+ impactFactors.push({
294
+ factor: 'shared-utility',
295
+ weight: 0.9,
296
+ reason: '共享工具/Hook 被多处引用',
297
+ });
298
+ }
299
+ // 3. 入口文件
300
+ if (this.graph.entryPoints.includes(file)) {
301
+ impactFactors.push({
302
+ factor: 'entry-point',
303
+ weight: 1.0,
304
+ reason: '应用入口文件,影响整个应用',
305
+ });
306
+ }
307
+ affectedFiles.push({
308
+ file,
309
+ kind: node.kind,
310
+ changeType: change.type,
311
+ impactLevel: depth,
312
+ impactFactors,
313
+ line: node.line,
314
+ });
315
+ }
316
+ return affectedFiles.sort((a, b) => a.impactLevel - b.impactLevel);
317
+ }
318
+ /**
319
+ * 评估边类型的影响
320
+ */
321
+ evaluateEdgeType(edgeType, depth) {
322
+ const weights = {
323
+ import: { weight: 0.7, reason: '模块导入依赖' },
324
+ extend: { weight: 0.9, reason: '类继承,可能有覆写' },
325
+ implement: { weight: 0.9, reason: '接口实现,必须兼容' },
326
+ 'type-ref': { weight: 0.8, reason: '类型引用,需保持兼容' },
327
+ call: { weight: 0.6, reason: '函数调用,可能有副作用' },
328
+ 'property-access': { weight: 0.4, reason: '属性访问,影响较轻' },
329
+ 'param-type': { weight: 0.7, reason: '参数类型约束' },
330
+ };
331
+ const info = weights[edgeType];
332
+ if (!info)
333
+ return null;
334
+ return {
335
+ factor: edgeType,
336
+ weight: info.weight * (1 - depth * 0.1), // 深度越大,权重递减
337
+ reason: info.reason,
338
+ };
339
+ }
340
+ /**
341
+ * 计算传播路径
342
+ */
343
+ computePropagationPaths(startNodes, affected) {
344
+ const paths = [];
345
+ for (const [nodeId, { path, edgeTypes, depth }] of affected.entries()) {
346
+ if (path.length < 2)
347
+ continue;
348
+ paths.push({
349
+ from: path[0],
350
+ to: path[path.length - 1],
351
+ path,
352
+ edgeTypes,
353
+ riskLevel: this.calculateRiskLevel(depth, edgeTypes),
354
+ });
355
+ }
356
+ return paths.sort((a, b) => {
357
+ const riskOrder = { critical: 0, high: 1, medium: 2, low: 3 };
358
+ return riskOrder[a.riskLevel] - riskOrder[b.riskLevel];
359
+ });
360
+ }
361
+ /**
362
+ * 计算风险等级
363
+ */
364
+ calculateRiskLevel(depth, edgeTypes) {
365
+ if (depth === 0)
366
+ return 'critical';
367
+ // 关键边类型
368
+ const criticalTypes = ['extend', 'implement', 'type-ref', 'call'];
369
+ const hasCritical = edgeTypes.some((t) => criticalTypes.includes(t));
370
+ if (hasCritical && depth <= 1)
371
+ return 'high';
372
+ if (depth <= 2)
373
+ return 'medium';
374
+ return 'low';
375
+ }
376
+ /**
377
+ * 识别高风险影响
378
+ */
379
+ identifyHighRiskImpacts(affected, change) {
380
+ const highRisks = [];
381
+ for (const [nodeId, { depth, edgeTypes }] of affected.entries()) {
382
+ const riskLevel = this.calculateRiskLevel(depth, edgeTypes);
383
+ if (riskLevel === 'critical' || riskLevel === 'high') {
384
+ const node = affected.get(nodeId).node;
385
+ const file = node.file || nodeId;
386
+ highRisks.push({
387
+ file,
388
+ reason: this.explainRisk(file, depth, edgeTypes),
389
+ riskLevel,
390
+ mitigation: this.suggestMitigation(file, edgeTypes),
391
+ });
392
+ }
393
+ }
394
+ return highRisks.sort((a, b) => {
395
+ const riskOrder = { critical: 0, high: 1, medium: 2, low: 3 };
396
+ return riskOrder[a.riskLevel] - riskOrder[b.riskLevel];
397
+ });
398
+ }
399
+ /**
400
+ * 解释风险
401
+ */
402
+ explainRisk(file, depth, edgeTypes) {
403
+ const typeStr = edgeTypes.join(', ');
404
+ if (depth === 0) {
405
+ return `${file} 是直接改动的文件`;
406
+ }
407
+ return `${file} 通过 [${typeStr}] 依赖链受到影响,深度 ${depth}`;
408
+ }
409
+ /**
410
+ * 建议缓解措施
411
+ */
412
+ suggestMitigation(file, edgeTypes) {
413
+ if (edgeTypes.includes('extend')) {
414
+ return '确保子类覆写方法时保持兼容性,考虑使用模板方法模式';
415
+ }
416
+ if (edgeTypes.includes('implement')) {
417
+ return '新增接口方法时提供默认实现,避免破坏现有实现';
418
+ }
419
+ if (edgeTypes.includes('type-ref')) {
420
+ return '类型变更需注意向后兼容,考虑使用联合类型或泛型';
421
+ }
422
+ if (edgeTypes.includes('call')) {
423
+ return '函数改动注意副作用,建议添加参数校验和错误处理';
424
+ }
425
+ return '建议进行全面的回归测试';
426
+ }
427
+ /**
428
+ * 估算破坏性变更数量
429
+ */
430
+ estimateBreakingChanges(affected) {
431
+ let count = 0;
432
+ for (const { node, depth } of affected.values()) {
433
+ // 破坏性变更的可能性评估
434
+ const isBreaking = (node.kind === 'interface' || node.kind === 'type') && depth <= 2 ||
435
+ node.kind === 'function' && depth === 1 ||
436
+ this.graph.entryPoints.includes(node.file || '');
437
+ if (isBreaking)
438
+ count++;
439
+ }
440
+ return count;
441
+ }
442
+ /**
443
+ * 生成建议
444
+ */
445
+ generateRecommendations(affected, change, stats) {
446
+ const recommendations = [];
447
+ // 基于统计的建议
448
+ if (stats.totalAffectedFiles > 20) {
449
+ recommendations.push(`⚠️ 影响范围较大 (${stats.totalAffectedFiles} 个文件),建议分阶段发布`);
450
+ }
451
+ if (stats.criticalFiles > 0) {
452
+ recommendations.push(`🚨 涉及 ${stats.criticalFiles} 个关键文件,需重点测试`);
453
+ }
454
+ if (stats.estimatedRippleDepth > 3) {
455
+ recommendations.push(`📊 影响链深度较深 (${stats.estimatedRippleDepth}),涟漪效应风险较高`);
456
+ }
457
+ // 基于改动类型的建议
458
+ switch (change.type) {
459
+ case 'delete':
460
+ recommendations.push('🗑️ 删除操作影响最广,确保无其他代码依赖该模块', '建议使用 IDE 的 "Find Usages" 功能确认无遗漏');
461
+ break;
462
+ case 'rename':
463
+ recommendations.push('✏️ 重命名操作需同步更新所有引用', '建议使用 IDE 的重命名重构功能,自动更新所有引用');
464
+ break;
465
+ case 'modify':
466
+ recommendations.push('🔧 修改操作注意保持 API 兼容性', '若需破坏性变更,建议创建新的 API 而非修改现有 API');
467
+ break;
468
+ }
469
+ // 去重
470
+ return this.deduplicateRecommendations(recommendations);
471
+ }
472
+ /**
473
+ * 去重建议
474
+ */
475
+ deduplicateRecommendations(recommendations) {
476
+ return [...new Set(recommendations)];
477
+ }
478
+ /**
479
+ * 简单 glob 模式匹配
480
+ */
481
+ matchPattern(file, pattern) {
482
+ const regex = pattern
483
+ .replace(/\./g, '\\.')
484
+ .replace(/\*\*/g, '.*')
485
+ .replace(/\*/g, '[^/]*');
486
+ return new RegExp(`^${regex}$`).test(file);
487
+ }
488
+ /**
489
+ * 创建空的影响范围
490
+ */
491
+ createEmptyScope(change) {
492
+ return {
493
+ changedFile: change.file,
494
+ timestamp: new Date().toISOString(),
495
+ stats: {
496
+ totalAffectedFiles: 0,
497
+ directDependencies: 0,
498
+ transitiveDependencies: 0,
499
+ filesWithBreakingChanges: 0,
500
+ criticalFiles: 0,
501
+ estimatedRippleDepth: 0,
502
+ },
503
+ levels: [],
504
+ affectedFiles: [],
505
+ propagationPaths: [],
506
+ highRiskImpacts: [],
507
+ recommendations: ['未找到相关依赖图节点,请确认文件路径正确'],
508
+ };
509
+ }
510
+ }
@@ -0,0 +1,63 @@
1
+ /**
2
+ * CallStackBuilder - 调用栈视图构建器
3
+ *
4
+ * 从改动点向上追踪,构建完整的调用链视图
5
+ */
6
+ export interface CallStackNode {
7
+ name: string;
8
+ file: string;
9
+ line: number;
10
+ type: 'function' | 'arrow' | 'method' | 'component';
11
+ children: CallStackNode[];
12
+ /** 调用此函数的代码(父节点的调用点) */
13
+ callSite?: {
14
+ line: number;
15
+ expression: string;
16
+ };
17
+ }
18
+ export interface CallStackTree {
19
+ root: CallStackNode;
20
+ depth: number;
21
+ path: string[];
22
+ }
23
+ export declare class CallStackBuilder {
24
+ private project;
25
+ private projectRoot;
26
+ constructor(projectRoot: string, tsConfigPath: string);
27
+ /**
28
+ * 添加源文件到项目
29
+ */
30
+ addSourceFiles(patterns: string[]): void;
31
+ /**
32
+ * 构建调用栈视图(从改动点向上追踪到入口)
33
+ */
34
+ buildCallStack(targetSymbol: string, targetFile: string): CallStackTree | null;
35
+ /**
36
+ * 查找符号定义
37
+ */
38
+ private findSymbolDefinition;
39
+ /**
40
+ * 递归追踪调用者
41
+ */
42
+ private traceCallers;
43
+ /**
44
+ * 查找调用某个函数的所有地方
45
+ */
46
+ private findCallers;
47
+ /**
48
+ * 在节点内查找对某个符号的调用
49
+ */
50
+ private findCallsInNode;
51
+ /**
52
+ * 计算树深度
53
+ */
54
+ private calculateDepth;
55
+ /**
56
+ * 构建路径字符串
57
+ */
58
+ private buildPathString;
59
+ /**
60
+ * 生成文本格式的调用栈视图
61
+ */
62
+ formatAsText(tree: CallStackTree, changedSymbol: string): string;
63
+ }