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,584 @@
1
+ /**
2
+ * DependencyGraph - 依赖图可视化
3
+ *
4
+ * 生成交互式依赖图和影响链图形
5
+ */
6
+
7
+ import * as path from 'path';
8
+ import type { ReferenceInfo, SymbolInfo } from './SymbolAnalyzer.js';
9
+
10
+ export interface GraphNode {
11
+ id: string;
12
+ label: string;
13
+ type: 'file' | 'symbol' | 'category';
14
+ category?: string;
15
+ risk?: 'low' | 'medium' | 'high' | 'critical';
16
+ impact?: number;
17
+ }
18
+
19
+ export interface GraphEdge {
20
+ source: string;
21
+ target: string;
22
+ type: 'import' | 'call' | 'type' | 'export' | 'property';
23
+ weight?: number;
24
+ }
25
+
26
+ export interface DependencyGraph {
27
+ nodes: GraphNode[];
28
+ edges: GraphEdge[];
29
+ }
30
+
31
+ export class DependencyGraphBuilder {
32
+ /**
33
+ * 构建依赖图
34
+ */
35
+ build(
36
+ symbolInfo: SymbolInfo,
37
+ references: ReferenceInfo[],
38
+ changedFile: string
39
+ ): DependencyGraph {
40
+ const nodes: GraphNode[] = [];
41
+ const edges: GraphEdge[] = [];
42
+ const nodeMap = new Map<string, GraphNode>();
43
+
44
+ // 添加变更文件的节点
45
+ const changedNode: GraphNode = {
46
+ id: changedFile,
47
+ label: path.basename(changedFile),
48
+ type: 'file',
49
+ category: this.categorizeFile(changedFile),
50
+ risk: 'high',
51
+ impact: 100,
52
+ };
53
+ nodes.push(changedNode);
54
+ nodeMap.set(changedFile, changedNode);
55
+
56
+ // 按文件分组引用
57
+ const refsByFile = new Map<string, ReferenceInfo[]>();
58
+ for (const ref of references) {
59
+ const file = ref.location.file;
60
+ if (!refsByFile.has(file)) {
61
+ refsByFile.set(file, []);
62
+ }
63
+ refsByFile.get(file)!.push(ref);
64
+ }
65
+
66
+ // 添加引用文件的节点和边
67
+ for (const [file, refs] of refsByFile) {
68
+ if (file === changedFile) continue;
69
+
70
+ const category = this.categorizeFile(file);
71
+
72
+ // 计算文件影响度
73
+ const impact = refs.length * 5;
74
+
75
+ const fileNode: GraphNode = {
76
+ id: file,
77
+ label: path.basename(file),
78
+ type: 'file',
79
+ category,
80
+ impact,
81
+ };
82
+ nodes.push(fileNode);
83
+ nodeMap.set(file, fileNode);
84
+
85
+ // 创建边
86
+ for (const ref of refs) {
87
+ const edgeType = this.mapReferenceType(ref.referenceType);
88
+ const weight = this.getEdgeWeight(ref.referenceType);
89
+
90
+ edges.push({
91
+ source: changedFile,
92
+ target: file,
93
+ type: edgeType,
94
+ weight,
95
+ });
96
+ }
97
+ }
98
+
99
+ return { nodes, edges };
100
+ }
101
+
102
+ /**
103
+ * 分类文件
104
+ */
105
+ private categorizeFile(filePath: string): string {
106
+ if (filePath.includes('/api/')) return 'API';
107
+ if (filePath.includes('/components/')) return 'Component';
108
+ if (filePath.includes('/pages/') || filePath.includes('/views/')) return 'Page';
109
+ if (filePath.includes('/hooks/')) return 'Hook';
110
+ if (filePath.includes('/utils/')) return 'Utility';
111
+ if (filePath.includes('/store/') || filePath.includes('/redux') || filePath.includes('/mobx')) return 'State';
112
+ if (filePath.includes('/context') || filePath.includes('/Context')) return 'Context';
113
+ if (filePath.includes('/types/')) return 'Type';
114
+ return 'Other';
115
+ }
116
+
117
+ /**
118
+ * 映射引用类型到边类型
119
+ */
120
+ private mapReferenceType(refType: string): GraphEdge['type'] {
121
+ switch (refType) {
122
+ case 'import':
123
+ return 'import';
124
+ case 'call':
125
+ return 'call';
126
+ case 'type':
127
+ return 'type';
128
+ case 'export':
129
+ return 'export';
130
+ case 'property':
131
+ return 'property';
132
+ default:
133
+ return 'import';
134
+ }
135
+ }
136
+
137
+ /**
138
+ * 获取边权重
139
+ */
140
+ private getEdgeWeight(refType: string): number {
141
+ switch (refType) {
142
+ case 'call': return 10;
143
+ case 'type': return 8;
144
+ case 'export': return 20;
145
+ case 'property': return 5;
146
+ default: return 1;
147
+ }
148
+ }
149
+
150
+ /**
151
+ * 生成交互式 HTML 图表
152
+ */
153
+ generateInteractiveHtml(graph: DependencyGraph, title: string): string {
154
+ const nodesJson = JSON.stringify(graph.nodes);
155
+ const edgesJson = JSON.stringify(graph.edges);
156
+
157
+ return `<!DOCTYPE html>
158
+ <html>
159
+ <head>
160
+ <meta charset="UTF-8">
161
+ <title>${title} - Dependency Graph</title>
162
+ <script src="https://d3js.org/d3.v7.min.js"></script>
163
+ <style>
164
+ body {
165
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
166
+ margin: 0;
167
+ padding: 20px;
168
+ background: #1a1a2e;
169
+ color: #eee;
170
+ }
171
+ h1 {
172
+ color: #00d4ff;
173
+ border-bottom: 2px solid #00d4ff;
174
+ padding-bottom: 10px;
175
+ }
176
+ #graph {
177
+ width: 100%;
178
+ height: 600px;
179
+ border: 1px solid #333;
180
+ border-radius: 8px;
181
+ background: #16213e;
182
+ }
183
+ .node {
184
+ cursor: pointer;
185
+ }
186
+ .node circle {
187
+ stroke: #fff;
188
+ stroke-width: 2px;
189
+ transition: all 0.3s;
190
+ }
191
+ .node:hover circle {
192
+ stroke: #00d4ff;
193
+ stroke-width: 3px;
194
+ }
195
+ .node text {
196
+ font-size: 10px;
197
+ fill: #fff;
198
+ pointer-events: none;
199
+ }
200
+ .link {
201
+ stroke: #4a5568;
202
+ stroke-opacity: 0.6;
203
+ fill: none;
204
+ }
205
+ .link.call { stroke: #48bb78; }
206
+ .link.type { stroke: #4299e1; }
207
+ .link.export { stroke: #ed8936; }
208
+ .link.property { stroke: #9f7aea; }
209
+ .link.import { stroke: #718096; }
210
+
211
+ .legend {
212
+ position: absolute;
213
+ top: 20px;
214
+ right: 20px;
215
+ background: #16213e;
216
+ padding: 15px;
217
+ border-radius: 8px;
218
+ border: 1px solid #333;
219
+ }
220
+ .legend-item {
221
+ display: flex;
222
+ align-items: center;
223
+ margin: 5px 0;
224
+ }
225
+ .legend-color {
226
+ width: 20px;
227
+ height: 3px;
228
+ margin-right: 10px;
229
+ border-radius: 2px;
230
+ }
231
+ .legend-color.call { background: #48bb78; }
232
+ .legend-color.type { background: #4299e1; }
233
+ .legend-color.export { background: #ed8936; }
234
+ .legend-color.property { background: #9f7aea; }
235
+ .legend-color.import { background: #718096; }
236
+
237
+ .tooltip {
238
+ position: absolute;
239
+ background: #2d3748;
240
+ padding: 10px;
241
+ border-radius: 4px;
242
+ font-size: 12px;
243
+ pointer-events: none;
244
+ opacity: 0;
245
+ transition: opacity 0.2s;
246
+ }
247
+ .tooltip.visible {
248
+ opacity: 1;
249
+ }
250
+
251
+ .stats {
252
+ display: flex;
253
+ gap: 20px;
254
+ margin: 20px 0;
255
+ }
256
+ .stat {
257
+ background: #16213e;
258
+ padding: 15px 25px;
259
+ border-radius: 8px;
260
+ border: 1px solid #333;
261
+ }
262
+ .stat-value {
263
+ font-size: 2em;
264
+ font-weight: bold;
265
+ color: #00d4ff;
266
+ }
267
+ .stat-label {
268
+ color: #888;
269
+ font-size: 0.9em;
270
+ }
271
+
272
+ .category-API { fill: #6f42c1; }
273
+ .category-Component { fill: #20c997; }
274
+ .category-Page { fill: #007bff; }
275
+ .category-Hook { fill: #fd7e14; }
276
+ .category-Utility { fill: #6c757d; }
277
+ .category-State { fill: #dc3545; }
278
+ .category-Context { fill: #e83e8c; }
279
+ .category-Type { fill: #17a2b8; }
280
+ .category-Other { fill: #6c757d; }
281
+ </style>
282
+ </head>
283
+ <body>
284
+ <h1>💥 ${title}</h1>
285
+
286
+ <div class="stats">
287
+ <div class="stat">
288
+ <div class="stat-value">${graph.nodes.length}</div>
289
+ <div class="stat-label">Nodes</div>
290
+ </div>
291
+ <div class="stat">
292
+ <div class="stat-value">${graph.edges.length}</div>
293
+ <div class="stat-label">Edges</div>
294
+ </div>
295
+ </div>
296
+
297
+ <div id="graph"></div>
298
+
299
+ <div class="legend">
300
+ <div class="legend-item"><div class="legend-color call"></div>Call</div>
301
+ <div class="legend-item"><div class="legend-color type"></div>Type Reference</div>
302
+ <div class="legend-item"><div class="legend-color export"></div>Export</div>
303
+ <div class="legend-item"><div class="legend-color property"></div>Property Access</div>
304
+ <div class="legend-item"><div class="legend-color import"></div>Import</div>
305
+ </div>
306
+
307
+ <div class="tooltip" id="tooltip"></div>
308
+
309
+ <script>
310
+ const nodes = ${nodesJson};
311
+ const edges = ${edgesJson};
312
+
313
+ const width = document.getElementById('graph').clientWidth;
314
+ const height = document.getElementById('graph').clientHeight;
315
+
316
+ const svg = d3.select('#graph')
317
+ .append('svg')
318
+ .attr('width', width)
319
+ .attr('height', height);
320
+
321
+ // 创建缩放行为
322
+ const zoom = d3.zoom()
323
+ .scaleExtent([0.1, 4])
324
+ .on('zoom', (event) => {
325
+ g.attr('transform', event.transform);
326
+ });
327
+
328
+ svg.call(zoom);
329
+
330
+ const g = svg.append('g');
331
+
332
+ // 力导向图
333
+ const simulation = d3.forceSimulation(nodes)
334
+ .force('link', d3.forceLink(edges).id(d => d.id).distance(120))
335
+ .force('charge', d3.forceManyBody().strength(-400))
336
+ .force('center', d3.forceCenter(width / 2, height / 2))
337
+ .force('collision', d3.forceCollide().radius(40));
338
+
339
+ // 绘制边
340
+ const link = g.append('g')
341
+ .selectAll('line')
342
+ .data(edges)
343
+ .join('line')
344
+ .attr('class', d => 'link ' + d.type)
345
+ .attr('stroke-width', d => d.weight || 1);
346
+
347
+ // 绘制节点
348
+ const node = g.append('g')
349
+ .selectAll('g')
350
+ .data(nodes)
351
+ .join('g')
352
+ .attr('class', 'node')
353
+ .call(d3.drag()
354
+ .on('start', dragstarted)
355
+ .on('drag', dragged)
356
+ .on('end', dragended));
357
+
358
+ node.append('circle')
359
+ .attr('r', d => d.impact ? Math.min(20, 8 + d.impact / 10) : 12)
360
+ .attr('fill', d => {
361
+ const cat = d.category || 'Other';
362
+ const colors = {
363
+ 'API': '#6f42c1',
364
+ 'Component': '#20c997',
365
+ 'Page': '#007bff',
366
+ 'Hook': '#fd7e14',
367
+ 'Utility': '#6c757d',
368
+ 'State': '#dc3545',
369
+ 'Context': '#e83e8c',
370
+ 'Type': '#17a2b8',
371
+ 'Other': '#6c757d'
372
+ };
373
+ return colors[cat] || '#6c757d';
374
+ });
375
+
376
+ node.append('text')
377
+ .attr('dx', 15)
378
+ .attr('dy', 4)
379
+ .text(d => d.label);
380
+
381
+ // 节点悬停提示
382
+ const tooltip = d3.select('#tooltip');
383
+
384
+ node.on('mouseover', (event, d) => {
385
+ tooltip.classed('visible', true)
386
+ .html('<strong>' + d.label + '</strong><br/>' +
387
+ 'Type: ' + (d.category || 'Unknown') + '<br/>' +
388
+ 'Impact: ' + (d.impact || 0));
389
+ tooltip.style('left', (event.pageX + 10) + 'px')
390
+ .style('top', (event.pageY - 10) + 'px');
391
+ })
392
+ .on('mouseout', () => {
393
+ tooltip.classed('visible', false);
394
+ });
395
+
396
+ simulation.on('tick', () => {
397
+ link
398
+ .attr('x1', d => d.source.x)
399
+ .attr('y1', d => d.source.y)
400
+ .attr('x2', d => d.target.x)
401
+ .attr('y2', d => d.target.y);
402
+
403
+ node.attr('transform', d => 'translate(' + d.x + ',' + d.y + ')');
404
+ });
405
+
406
+ function dragstarted(event) {
407
+ if (!event.active) simulation.alphaTarget(0.3).restart();
408
+ event.subject.fx = event.subject.x;
409
+ event.subject.fy = event.subject.y;
410
+ }
411
+
412
+ function dragged(event) {
413
+ event.subject.fx = event.x;
414
+ event.subject.fy = event.y;
415
+ }
416
+
417
+ function dragended(event) {
418
+ if (!event.active) simulation.alphaTarget(0);
419
+ event.subject.fx = null;
420
+ event.subject.fy = null;
421
+ }
422
+ </script>
423
+ </body>
424
+ </html>`;
425
+ }
426
+
427
+ /**
428
+ * 生成传播路径图
429
+ */
430
+ generatePropagationHtml(
431
+ paths: Array<{ from: string; to: string; path: string[]; type: string }>,
432
+ title: string
433
+ ): string {
434
+ if (paths.length === 0) {
435
+ return `<html><body style="font-family: sans-serif; padding: 20px;">
436
+ <h2>${title}</h2>
437
+ <p>No propagation paths found.</p>
438
+ </body></html>`;
439
+ }
440
+
441
+ // 简化版本:使用节点列表和边
442
+ const nodes: GraphNode[] = [];
443
+ const edges: GraphEdge[] = [];
444
+ const nodeSet = new Set<string>();
445
+
446
+ for (const p of paths) {
447
+ for (const n of p.path) {
448
+ nodeSet.add(n);
449
+ }
450
+ }
451
+
452
+ let nodeId = 0;
453
+ const nodeMap = new Map<string, string>();
454
+ for (const n of nodeSet) {
455
+ const id = 'node_' + nodeId++;
456
+ nodeMap.set(n, id);
457
+ nodes.push({
458
+ id,
459
+ label: n,
460
+ type: 'symbol',
461
+ });
462
+ }
463
+
464
+ for (const p of paths) {
465
+ for (let i = 0; i < p.path.length - 1; i++) {
466
+ edges.push({
467
+ source: nodeMap.get(p.path[i])!,
468
+ target: nodeMap.get(p.path[i + 1])!,
469
+ type: p.type as GraphEdge['type'],
470
+ });
471
+ }
472
+ }
473
+
474
+ const nodesJson = JSON.stringify(nodes);
475
+ const edgesJson = JSON.stringify(edges);
476
+
477
+ return `<!DOCTYPE html>
478
+ <html>
479
+ <head>
480
+ <meta charset="UTF-8">
481
+ <title>${title} - Propagation Paths</title>
482
+ <script src="https://d3js.org/d3.v7.min.js"></script>
483
+ <style>
484
+ body {
485
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
486
+ margin: 0;
487
+ padding: 20px;
488
+ background: #1a1a2e;
489
+ color: #eee;
490
+ }
491
+ h1 {
492
+ color: #00d4ff;
493
+ border-bottom: 2px solid #00d4ff;
494
+ padding-bottom: 10px;
495
+ }
496
+ #graph {
497
+ width: 100%;
498
+ height: 500px;
499
+ border: 1px solid #333;
500
+ border-radius: 8px;
501
+ background: #16213e;
502
+ }
503
+ .node circle {
504
+ fill: #00d4ff;
505
+ stroke: #fff;
506
+ stroke-width: 2px;
507
+ }
508
+ .node text {
509
+ font-size: 12px;
510
+ fill: #fff;
511
+ }
512
+ .link {
513
+ stroke: #4a5568;
514
+ stroke-width: 2px;
515
+ fill: none;
516
+ marker-end: url(#arrowhead);
517
+ }
518
+ .link.call { stroke: #48bb78; }
519
+ .link.type { stroke: #4299e1; }
520
+ svg {
521
+ width: 100%;
522
+ height: 100%;
523
+ }
524
+ </style>
525
+ </head>
526
+ <body>
527
+ <h1>🔗 ${title} - Impact Propagation</h1>
528
+ <p>Showing how changes flow through the codebase</p>
529
+ <div id="graph">
530
+ <svg>
531
+ <defs>
532
+ <marker id="arrowhead" viewBox="0 0 10 10" refX="20" refY="5"
533
+ markerWidth="6" markerHeight="6" orient="auto">
534
+ <path d="M 0 0 L 10 5 L 0 10 z" fill="#718096" />
535
+ </marker>
536
+ </defs>
537
+ </svg>
538
+ </div>
539
+ <script>
540
+ const nodes = ${nodesJson};
541
+ const edges = ${edgesJson};
542
+
543
+ const svg = d3.select('#graph svg');
544
+ const width = document.getElementById('graph').clientWidth;
545
+ const height = document.getElementById('graph').clientHeight;
546
+
547
+ const g = svg.append('g');
548
+
549
+ // 力导向图
550
+ const simulation = d3.forceSimulation(nodes)
551
+ .force('link', d3.forceLink(edges).id(d => d.id).distance(100))
552
+ .force('charge', d3.forceManyBody().strength(-300))
553
+ .force('center', d3.forceCenter(width / 2, height / 2));
554
+
555
+ const link = g.selectAll('.link')
556
+ .data(edges)
557
+ .join('path')
558
+ .attr('class', d => 'link ' + d.type);
559
+
560
+ const node = g.selectAll('.node')
561
+ .data(nodes)
562
+ .join('g')
563
+ .attr('class', 'node');
564
+
565
+ node.append('circle').attr('r', 10);
566
+ node.append('text')
567
+ .attr('dy', 4)
568
+ .attr('dx', 15)
569
+ .text(d => d.label);
570
+
571
+ simulation.on('tick', () => {
572
+ link.attr('d', d => {
573
+ const dx = d.target.x - d.source.x;
574
+ const dy = d.target.y - d.source.y;
575
+ return 'M' + d.source.x + ',' + d.source.y + ' L' + d.target.x + ',' + d.target.y;
576
+ });
577
+
578
+ node.attr('transform', d => 'translate(' + d.x + ',' + d.y + ')');
579
+ });
580
+ </script>
581
+ </body>
582
+ </html>`;
583
+ }
584
+ }