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
package/src/index.ts ADDED
@@ -0,0 +1,1071 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Blast Radius Analyzer - 改动影响范围分析器
4
+ *
5
+ * 使用符号级分析 + 数据流追踪
6
+ * 支持任意 JavaScript/TypeScript 项目
7
+ *
8
+ * Usage:
9
+ * blast-radius --project ./src --change src/api/user.ts
10
+ * blast-radius --project ./src --change src/utils/helper.ts --symbol helperFunc
11
+ * blast-radius --project ./src --change src/api/task.ts --graph
12
+ */
13
+
14
+ import * as path from 'path';
15
+ import * as fs from 'fs';
16
+ import { ImpactTracer } from './core/ImpactTracer.js';
17
+ import { AnalysisCache } from './core/AnalysisCache.js';
18
+ import { DependencyGraphBuilder } from './core/DependencyGraph.js';
19
+ import { PropertyAccessTracker } from './core/PropertyAccessTracker.js';
20
+ import { CallStackBuilder } from './core/CallStackBuilder.js';
21
+ import { TypeFlowAnalyzer } from './core/TypeFlowAnalyzer.js';
22
+ import { DataFlowAnalyzer } from './core/DataFlowAnalyzer.js';
23
+ import type { AnalyzerConfig } from './types.js';
24
+
25
+ // ─── CLI 参数解析 ────────────────────────────────────────────────────────────
26
+
27
+ interface CLIArgs {
28
+ projectRoot: string;
29
+ tsConfig: string;
30
+ changes: Array<{
31
+ file: string;
32
+ symbol?: string;
33
+ type: 'modify' | 'delete' | 'rename' | 'add';
34
+ }>;
35
+ maxDepth: number;
36
+ includeTests: boolean;
37
+ verbose: boolean;
38
+ output: string | null;
39
+ format: 'json' | 'text' | 'html' | 'graph';
40
+ useCache: boolean;
41
+ clearCache: boolean;
42
+ threshold?: {
43
+ files?: number;
44
+ score?: number;
45
+ typeErrors?: number;
46
+ };
47
+ }
48
+
49
+ function parseArgs(argv: string[]): CLIArgs {
50
+ let projectRoot = process.cwd();
51
+ let tsConfig = '';
52
+ const changes: CLIArgs['changes'] = [];
53
+ let maxDepth = 10;
54
+ let includeTests = false;
55
+ let verbose = false;
56
+ let output: string | null = null;
57
+ let format: 'json' | 'text' | 'html' | 'graph' = 'text';
58
+ let useCache = true;
59
+ let clearCache = false;
60
+ let threshold: CLIArgs['threshold'] = undefined;
61
+
62
+ for (let i = 2; i < argv.length; i++) {
63
+ const arg = argv[i];
64
+
65
+ if ((arg === '--project' || arg === '-p') && argv[i + 1]) {
66
+ projectRoot = path.resolve(argv[++i]);
67
+ tsConfig = `${projectRoot}/tsconfig.json`;
68
+ } else if (arg === '--tsconfig' && argv[i + 1]) {
69
+ tsConfig = path.resolve(argv[++i]);
70
+ } else if ((arg === '--change' || arg === '-c') && argv[i + 1]) {
71
+ const changePath = path.resolve(projectRoot, argv[++i]);
72
+ changes.push({ file: changePath, type: 'modify' });
73
+ } else if (arg === '--symbol' && argv[i + 1]) {
74
+ if (changes.length > 0) {
75
+ changes[changes.length - 1].symbol = argv[++i];
76
+ }
77
+ } else if (arg === '--type' && argv[i + 1]) {
78
+ if (changes.length > 0) {
79
+ changes[changes.length - 1].type = argv[++i] as 'modify' | 'delete' | 'rename' | 'add';
80
+ }
81
+ } else if (arg === '--max-depth' && argv[i + 1]) {
82
+ maxDepth = parseInt(argv[++i], 10);
83
+ } else if (arg === '--include-tests' || arg === '-t') {
84
+ includeTests = true;
85
+ } else if (arg === '--verbose' || arg === '-v') {
86
+ verbose = true;
87
+ } else if ((arg === '--output' || arg === '-o') && argv[i + 1]) {
88
+ output = argv[++i];
89
+ if (output.endsWith('.json')) format = 'json';
90
+ else if (output.endsWith('.svg')) format = 'graph';
91
+ // Don't set format to 'html' if --graph was already set
92
+ } else if (arg === '--graph') {
93
+ format = 'graph';
94
+ } else if (arg === '--no-cache') {
95
+ useCache = false;
96
+ } else if (arg === '--clear-cache') {
97
+ clearCache = true;
98
+ } else if (arg === '--threshold' && argv[i + 1]) {
99
+ // 解析阈值: --threshold files:5,score:100,typeErrors:0
100
+ const thresholdStr = argv[++i];
101
+ threshold = {};
102
+ for (const part of thresholdStr.split(',')) {
103
+ const [key, value] = part.split(':');
104
+ const num = parseInt(value, 10);
105
+ if (key === 'files') threshold.files = num;
106
+ else if (key === 'score') threshold.score = num;
107
+ else if (key === 'typeErrors') threshold.typeErrors = num;
108
+ }
109
+ } else if (arg === '--help' || arg === '-h') {
110
+ printHelp();
111
+ process.exit(0);
112
+ }
113
+ }
114
+
115
+ // Default to HTML if no format detected and not graph
116
+ if (format === 'text' && output?.endsWith('.html')) {
117
+ format = 'html';
118
+ }
119
+
120
+ return {
121
+ projectRoot,
122
+ tsConfig,
123
+ changes,
124
+ maxDepth,
125
+ includeTests,
126
+ verbose,
127
+ output,
128
+ format,
129
+ useCache,
130
+ clearCache,
131
+ threshold: threshold,
132
+ };
133
+ }
134
+
135
+ function printHelp(): void {
136
+ console.log(`
137
+ blast-radius - Code Change Impact Analyzer
138
+ ==========================================
139
+
140
+ Usage:
141
+ blast-radius [options]
142
+
143
+ Options:
144
+ -p, --project <path> Project root directory (default: cwd)
145
+ --tsconfig <path> Path to tsconfig.json
146
+ -c, --change <file> File that changed (can be specified multiple times)
147
+ --symbol <name> Specific symbol that changed (function/class name)
148
+ --type <type> Change type: add|modify|delete|rename (default: modify)
149
+ --max-depth <n> Maximum analysis depth (default: 10)
150
+ -t, --include-tests Include test files in analysis
151
+ -o, --output <file> Output file (auto-detect format from extension)
152
+ --format <format> Output format: json|text|html (default: text)
153
+ --graph Generate interactive dependency graph (outputs SVG)
154
+ --no-cache Disable incremental analysis cache
155
+ --clear-cache Clear analysis cache
156
+ --threshold <rules> CI/CD threshold alert (e.g., files:5,score:100,typeErrors:0)
157
+ -v, --verbose Verbose output
158
+ -h, --help Show this help
159
+
160
+ CI/CD Examples:
161
+ # Set threshold for auto-fail in CI
162
+ blast-radius -p ./src -c src/api/task.ts --threshold files:5,score:100,typeErrors:0
163
+
164
+ # Use with git hooks or CI pipelines
165
+ blast-radius -p ./src -c src/api/task.ts --threshold files:3 -o result.json
166
+
167
+ Examples:
168
+ # Analyze a single file change
169
+ blast-radius -p ./src -c src/api/user.ts
170
+
171
+ # Analyze a specific function change
172
+ blast-radius -p ./src -c src/utils/helper.ts --symbol helperFunc
173
+
174
+ # Analyze a delete operation
175
+ blast-radius -p ./src -c src/old/func.ts --type delete
176
+
177
+ # Output as JSON
178
+ blast-radius -p ./src -c src/api/task.ts -o result.json
179
+
180
+ # Output as HTML with dependency graph
181
+ blast-radius -p ./src -c src/utils/request.ts -o report.html --graph
182
+
183
+ # Use incremental analysis (default)
184
+ blast-radius -p ./src -c src/api/task.ts
185
+
186
+ # Clear cache and re-analyze
187
+ blast-radius -p ./src -c src/api/task.ts --clear-cache
188
+ `);
189
+ }
190
+
191
+ // ─── JSON序列化辅助 ──────────────────────────────────────────────────────────
192
+
193
+ /**
194
+ * 安全地序列化对象到JSON,移除循环引用和不可序列化的属性
195
+ */
196
+ function safeSerialize(obj: any, depth = 0): any {
197
+ if (depth > 20) return '[Max Depth Exceeded]';
198
+ if (obj === null || obj === undefined) return obj;
199
+ if (typeof obj === 'function') return '[Function]';
200
+ if (typeof obj === 'symbol') return '[Symbol]';
201
+
202
+ // 处理循环引用
203
+ if (depth > 0 && (obj instanceof Map || obj instanceof Set)) {
204
+ if (obj instanceof Map) {
205
+ return Array.from(obj.entries()).map(([k, v]) => [k, safeSerialize(v, depth + 1)]);
206
+ }
207
+ if (obj instanceof Set) {
208
+ return Array.from(obj.values()).map(v => safeSerialize(v, depth + 1));
209
+ }
210
+ }
211
+
212
+ // 处理数组
213
+ if (Array.isArray(obj)) {
214
+ return obj.map(item => safeSerialize(item, depth + 1));
215
+ }
216
+
217
+ // 处理普通对象
218
+ if (typeof obj === 'object') {
219
+ const result: any = {};
220
+ for (const key of Object.keys(obj)) {
221
+ try {
222
+ const value = obj[key];
223
+ // 跳过已知包含循环引用的属性
224
+ if (key === 'symbol' || key === 'declaration' || key === 'node' || key === 'checker') {
225
+ result[key] = '[Complex Object]';
226
+ } else if (value && typeof value === 'object') {
227
+ result[key] = safeSerialize(value, depth + 1);
228
+ } else {
229
+ result[key] = value;
230
+ }
231
+ } catch (e) {
232
+ result[key] = '[Serialization Error]';
233
+ }
234
+ }
235
+ return result;
236
+ }
237
+
238
+ return obj;
239
+ }
240
+
241
+ /**
242
+ * 提取scope中可序列化的部分用于JSON输出
243
+ */
244
+ function extractSerializableScope(scope: any): any {
245
+ if (!scope) return {};
246
+
247
+ return {
248
+ changedFile: scope.changedFile,
249
+ changedSymbol: scope.changedSymbol,
250
+ changeType: scope.changeType,
251
+ timestamp: scope.timestamp,
252
+ stats: safeSerialize(scope.stats),
253
+ affectedFiles: safeSerialize(scope.affectedFiles),
254
+ recommendations: Array.isArray(scope.recommendations) ? scope.recommendations : [],
255
+ downstreamChain: safeSerialize(scope.downstreamChain),
256
+ callGraph: scope.callGraph ? 'Map(complex object)' : undefined,
257
+ symbolInfo: scope.symbolInfo ? {
258
+ name: scope.symbolInfo.name,
259
+ kind: scope.symbolInfo.kind,
260
+ file: scope.symbolInfo.file,
261
+ line: scope.symbolInfo.line,
262
+ } : null,
263
+ typeDependencies: safeSerialize(scope.typeDependencies),
264
+ exportDependents: safeSerialize(scope.exportDependents),
265
+ };
266
+ }
267
+
268
+ // ─── 格式化输出 ──────────────────────────────────────────────────────────────
269
+
270
+ interface CallChainAnalysis {
271
+ file: string;
272
+ line: number;
273
+ callExpression: string;
274
+ returnedTo?: string;
275
+ propertyAccesses: Array<{
276
+ accessChain: string[];
277
+ line: number;
278
+ fullExpression: string;
279
+ }>;
280
+ }
281
+
282
+ function formatText(
283
+ scope: ReturnType<ImpactTracer['traceImpact']> extends Promise<infer T> ? T : never,
284
+ callChains?: CallChainAnalysis[],
285
+ callStackView?: { root: any; depth: number; path: string[] } | null,
286
+ typeFlowResult?: any,
287
+ dataFlowResult?: any
288
+ ): string {
289
+ const lines: string[] = [];
290
+
291
+ // 标题
292
+ lines.push('');
293
+ lines.push('┌─────────────────────────────────────────────────────────────────┐');
294
+ lines.push('│ 📊 改动影响范围分析报告 │');
295
+ lines.push('└─────────────────────────────────────────────────────────────────┘');
296
+ lines.push('');
297
+
298
+ // 改动摘要
299
+ const changedFileName = path.basename(scope.changedFile);
300
+ lines.push('📝 改动内容');
301
+ lines.push(' 文件: ' + changedFileName);
302
+ if (scope.changedSymbol) {
303
+ lines.push(' 符号: ' + scope.changedSymbol);
304
+ }
305
+ const changeTypeText: Record<string, string> = { modify: '修改', delete: '删除', rename: '重命名', add: '新增' };
306
+ lines.push(' 类型: ' + (changeTypeText[scope.changeType] || scope.changeType));
307
+ lines.push('');
308
+
309
+ // 风险等级 - 更直观
310
+ const riskConfig = {
311
+ low: { color: '🟢 低风险', desc: '影响范围小,可以放心发布', bg: '░░░░░░░░░' },
312
+ medium: { color: '🟡 中等风险', desc: '有部分影响,建议检查相关代码', bg: '██████░░░' },
313
+ high: { color: '🟠 高风险', desc: '影响范围较大,需要全面测试', bg: '████████░░' },
314
+ critical: { color: '🔴 极高风险', desc: '核心模块改动,必须谨慎处理', bg: '██████████' },
315
+ };
316
+ const risk = riskConfig[scope.stats.riskLevel] || riskConfig.low;
317
+ lines.push('🚨 风险等级: ' + risk.color);
318
+ lines.push(' ' + risk.desc);
319
+ lines.push(' 影响程度: ' + risk.bg + ' (' + scope.stats.impactScore + '分)');
320
+ lines.push('');
321
+
322
+ // 简洁的影响统计
323
+ lines.push('📈 影响范围');
324
+ lines.push(' ├─ 受影响文件: ' + scope.stats.totalAffectedFiles + ' 个');
325
+ lines.push(' ├─ 直接引用: ' + scope.stats.directReferences + ' 处');
326
+ lines.push(' └─ 调用点: ' + scope.stats.callSites + ' 处');
327
+ lines.push('');
328
+
329
+ // 调用链详情 - 更详细
330
+ if (callChains && callChains.length > 0) {
331
+ lines.push('🔗 调用链路详情');
332
+ lines.push('');
333
+ for (const chain of callChains) {
334
+ const fileName = path.basename(chain.file);
335
+ const filePath = chain.file.replace(/^.*[/\\]src[/\\]/, 'src/');
336
+ lines.push(' 📍 ' + fileName);
337
+ lines.push(' 路径: ' + filePath);
338
+ lines.push(' 行号: 第' + chain.line + '行');
339
+ lines.push(' 完整代码:');
340
+ lines.push(' ' + (chain as any).codeContext || chain.callExpression);
341
+ if (chain.returnedTo) {
342
+ lines.push(' 返回值赋给: ' + chain.returnedTo);
343
+ }
344
+ if (chain.propertyAccesses.length > 0) {
345
+ lines.push('');
346
+ lines.push(' └─ 属性使用:');
347
+ // 去重并显示
348
+ const uniqueChains = [...new Set(chain.propertyAccesses.map(p => p.accessChain.join('.')))];
349
+ for (const prop of uniqueChains) {
350
+ const propAccess = chain.propertyAccesses.find(p => p.accessChain.join('.') === prop);
351
+ const line = propAccess?.line;
352
+ const context = (propAccess as any)?.codeContext || propAccess?.fullExpression || prop;
353
+ lines.push(' • ' + prop);
354
+ lines.push(' 位置: 第' + line + '行');
355
+ lines.push(' 代码: ' + context);
356
+ }
357
+ }
358
+ lines.push('');
359
+ }
360
+ }
361
+
362
+ // 调用栈视图 - 从改动点向上追踪
363
+ if (callStackView && callStackView.depth > 0) {
364
+ lines.push('📞 调用栈视图');
365
+ lines.push('');
366
+
367
+ const renderNode = (node: any, prefix: string, isLast: boolean, isRoot: boolean) => {
368
+ const connector = isLast ? '└─' : '├─';
369
+ const current = isRoot
370
+ ? `📍 ${node.name} (改动点)`
371
+ : `${connector} ${node.name}`;
372
+ const typeTag = ` [${node.type || 'function'}]`;
373
+ lines.push(`${prefix}${current}${typeTag} → ${path.basename(node.file)}:${node.line}`);
374
+
375
+ if (node.callSite) {
376
+ lines.push(`${prefix} │`);
377
+ lines.push(`${prefix} └── 调用: 第${node.callSite.line}行 "${node.callSite.expression}"`);
378
+ }
379
+
380
+ const childPrefix = prefix + (isLast ? ' ' : '│ ');
381
+ if (node.children && node.children.length > 0) {
382
+ node.children.forEach((child: any, idx: number) => {
383
+ const isChildLast = idx === node.children.length - 1;
384
+ renderNode(child, childPrefix, isChildLast, false);
385
+ });
386
+ }
387
+ };
388
+
389
+ renderNode(callStackView.root, '', true, true);
390
+ lines.push('');
391
+ lines.push(` 深度: ${callStackView.depth} 层`);
392
+ lines.push('');
393
+ }
394
+
395
+ // 类型流分析 - 检测类型不兼容
396
+ if (typeFlowResult && typeFlowResult.method) {
397
+ lines.push('🔬 类型流分析');
398
+ lines.push('');
399
+ if (typeFlowResult.hasIncompatibilities) {
400
+ lines.push(` ⚠️ 检测到 ${typeFlowResult.incompatibilities.length} 处类型不兼容`);
401
+ lines.push(` 分析方法: ${typeFlowResult.method} | 可信度: ${typeFlowResult.confidence}`);
402
+ lines.push('');
403
+ for (const item of typeFlowResult.incompatibilities.slice(0, 5)) {
404
+ lines.push(` 📍 ${path.basename(item.file)}:${item.line}`);
405
+ lines.push(` 原因: ${item.reason}`);
406
+ if (item.propertyAccess && item.propertyAccess.length > 0) {
407
+ lines.push(` 属性: ${item.propertyAccess.join(' → ')}`);
408
+ }
409
+ lines.push('');
410
+ }
411
+ } else {
412
+ lines.push(' ✅ 未检测到类型不兼容');
413
+ lines.push(` 分析方法: ${typeFlowResult.method} | 可信度: ${typeFlowResult.confidence}`);
414
+ }
415
+ lines.push('');
416
+ }
417
+
418
+ // 数据流分析 - 追踪数据在程序中的流转
419
+ if (dataFlowResult && dataFlowResult.statistics) {
420
+ lines.push('📊 数据流分析 (DataFlow Pro)');
421
+ lines.push('');
422
+ lines.push(` 节点分析: ${dataFlowResult.statistics.nodesAnalyzed}`);
423
+ lines.push(` 路径追踪: ${dataFlowResult.statistics.pathsTracked}`);
424
+ lines.push(` 约束生成: ${dataFlowResult.statistics.constraintsGenerated}`);
425
+ lines.push(` Promise解包: ${dataFlowResult.statistics.promiseUnwraps}`);
426
+ lines.push(` 条件分支: ${dataFlowResult.statistics.conditionalBranches}`);
427
+ lines.push(` 类型收窄: ${dataFlowResult.statistics.typesNarrowed}`);
428
+ lines.push(` 置信度: ${dataFlowResult.confidence}`);
429
+ lines.push(` 耗时: ${dataFlowResult.duration}ms`);
430
+ lines.push('');
431
+
432
+ if (dataFlowResult.typeNarrowing && dataFlowResult.typeNarrowing.size > 0) {
433
+ lines.push(' 🔽 类型收窄:');
434
+ for (const [varName, narrowing] of dataFlowResult.typeNarrowing) {
435
+ lines.push(` ${varName}:`);
436
+ for (const n of narrowing) {
437
+ lines.push(` → 第${n.line}行: ${n.types.join(' | ')}`);
438
+ }
439
+ }
440
+ lines.push('');
441
+ }
442
+
443
+ if (dataFlowResult.flowPaths && dataFlowResult.flowPaths.length > 0) {
444
+ lines.push(' 🔗 数据流路径:');
445
+ for (const fp of dataFlowResult.flowPaths.slice(0, 3)) {
446
+ lines.push(` ${fp.source}`);
447
+ lines.push(` ↓ ${fp.typeAtSource}`);
448
+ for (const node of fp.path.slice(0, 3)) {
449
+ lines.push(` → ${node.expression} (${node.type})`);
450
+ }
451
+ lines.push(` → ${fp.sink}`);
452
+ lines.push('');
453
+ }
454
+ }
455
+ }
456
+
457
+ // 传播路径 - 对于非函数符号(如常量、类型)显示传播链
458
+ if (scope.propagationPaths && scope.propagationPaths.length > 0) {
459
+ lines.push('🔗 传播路径详情');
460
+ lines.push('');
461
+ for (const pp of scope.propagationPaths.slice(0, 10)) {
462
+ const fromName = pp.path[0] || pp.from;
463
+ const toName = path.basename(pp.to);
464
+ const chain = pp.path.join(' → ') || `${fromName} → ${toName}`;
465
+ lines.push(` 📍 ${chain}`);
466
+ lines.push(` 位置: ${toName}`);
467
+ lines.push(` 类型: ${pp.type}`);
468
+ lines.push('');
469
+ }
470
+ }
471
+
472
+ // 受影响的文件列表 - 更清晰
473
+ if (scope.affectedFiles.length > 0) {
474
+ lines.push('📁 受影响的文件');
475
+ for (const af of scope.affectedFiles.slice(0, 10)) {
476
+ const fileName = path.basename(af.file);
477
+ const refCount = af.references.length;
478
+ const fileType = af.category || '其他';
479
+
480
+ // 文件类型的中文
481
+ const fileTypeText: Record<string, string> = {
482
+ 'API Layer': 'API接口',
483
+ 'Page': '页面',
484
+ 'Component': '组件',
485
+ 'Hook': '钩子函数',
486
+ 'State Management': '状态管理',
487
+ 'Context': '上下文',
488
+ 'Utility': '工具函数',
489
+ 'Service': '服务',
490
+ 'Type Definition': '类型定义',
491
+ 'Other': '其他',
492
+ };
493
+
494
+ lines.push(' 📄 ' + fileName);
495
+ lines.push(' 位置: ' + fileTypeText[fileType] || fileType);
496
+ lines.push(' 引用: ' + refCount + ' 处');
497
+ lines.push('');
498
+ }
499
+ if (scope.affectedFiles.length > 10) {
500
+ lines.push(' ... 还有 ' + (scope.affectedFiles.length - 10) + ' 个文件受影响');
501
+ lines.push('');
502
+ }
503
+ }
504
+
505
+ // 改动建议
506
+ if (scope.recommendations.length > 0) {
507
+ lines.push('💡 建议');
508
+ for (const rec of scope.recommendations) {
509
+ // 去掉 emoji 前的特殊字符,只保留主要文字
510
+ const cleanRec = rec.replace(/^[^\u4e00-\u9fa5]*/g, '').trim();
511
+ lines.push(' • ' + cleanRec);
512
+ }
513
+ lines.push('');
514
+ }
515
+
516
+ // 简单总结
517
+ if (scope.stats.totalAffectedFiles <= 1 && scope.stats.callSites <= 2) {
518
+ lines.push('✅ 总结: 此改动影响很小,可以正常发布');
519
+ } else if (scope.stats.totalAffectedFiles <= 5) {
520
+ lines.push('⚠️ 总结: 此改动有少量影响,发布前建议检查相关文件');
521
+ } else {
522
+ lines.push('🔴 总结: 此改动影响范围较大,建议进行充分测试后再发布');
523
+ }
524
+ lines.push('');
525
+
526
+ return lines.join('\n');
527
+ }
528
+
529
+ /**
530
+ * 渲染调用栈HTML
531
+ */
532
+ function renderCallStackHtml(node: any, isRoot = false): string {
533
+ const typeColors: Record<string, string> = {
534
+ function: '#667eea',
535
+ arrow: '#11998e',
536
+ method: '#f5576c',
537
+ component: '#764ba2',
538
+ };
539
+ const color = typeColors[node.type] || '#667eea';
540
+
541
+ let html = `
542
+ <div class="stack-node ${isRoot ? 'stack-root' : ''}" style="border-left-color: ${color}">
543
+ <div class="stack-header">
544
+ <span class="stack-tag">${isRoot ? '📍' : '→'}</span>
545
+ <span class="stack-name">${node.name}</span>
546
+ ${isRoot ? '<span class="stack-root-badge">改动点</span>' : ''}
547
+ <span class="stack-type" style="background: ${color}">${node.type || 'function'}</span>
548
+ </div>
549
+ <div class="stack-location">${path.basename(node.file)}:${node.line}</div>`;
550
+
551
+ if (node.callSite) {
552
+ html += `
553
+ <div class="stack-call">
554
+ <code>第${node.callSite.line}行: ${node.callSite.expression}</code>
555
+ </div>`;
556
+ }
557
+
558
+ if (node.children && node.children.length > 0) {
559
+ html += `<div class="stack-children">`;
560
+ node.children.forEach((child: any) => {
561
+ html += renderCallStackHtml(child, false);
562
+ });
563
+ html += `</div>`;
564
+ }
565
+
566
+ html += `</div>`;
567
+ return html;
568
+ }
569
+
570
+ function formatHtml(
571
+ scope: ReturnType<ImpactTracer['traceImpact']> extends Promise<infer T> ? T : never,
572
+ callChains?: CallChainAnalysis[],
573
+ callStackView?: { root: any; depth: number; path: string[] } | null,
574
+ typeFlowResult?: any,
575
+ dataFlowResult?: any
576
+ ): string {
577
+ const riskConfig = {
578
+ low: { color: '#28a745', label: '低风险', bar: '░░░░░░░░░' },
579
+ medium: { color: '#ffc107', label: '中等风险', bar: '██████░░░' },
580
+ high: { color: '#fd7e14', label: '高风险', bar: '████████░░' },
581
+ critical: { color: '#dc3545', label: '极高风险', bar: '██████████' },
582
+ };
583
+ const risk = riskConfig[scope.stats.riskLevel] || riskConfig.low;
584
+
585
+ const fileTypeMap: Record<string, string> = {
586
+ 'API Layer': 'API接口',
587
+ 'Page': '页面',
588
+ 'Component': '组件',
589
+ 'Hook': '钩子函数',
590
+ 'State Management': '状态管理',
591
+ 'Context': '上下文',
592
+ 'Utility': '工具函数',
593
+ 'Service': '服务',
594
+ 'Type Definition': '类型定义',
595
+ 'Other': '其他',
596
+ };
597
+
598
+ // Build call chain HTML
599
+ let callChainHtml = '';
600
+ if (callChains && callChains.length > 0) {
601
+ callChainHtml = `
602
+ <h2>🔗 调用链路详情</h2>
603
+ <div class="call-chains">`;
604
+ for (const chain of callChains) {
605
+ const fileName = path.basename(chain.file);
606
+ const codeCtx = (chain as any).codeContext || chain.callExpression;
607
+ const uniqueProps = [...new Set(chain.propertyAccesses.map(p => p.accessChain.join('.')))];
608
+ callChainHtml += `
609
+ <div class="call-chain-item">
610
+ <div class="call-chain-header">
611
+ <span class="call-file">📍 ${fileName}</span>
612
+ <span class="call-line">第${chain.line}行</span>
613
+ </div>
614
+ <div class="code-context">
615
+ <div class="code-label">完整代码:</div>
616
+ <pre class="code-block">${codeCtx}</pre>
617
+ </div>
618
+ ${chain.returnedTo ? `<div class="call-return">返回值 → <code>${chain.returnedTo}</code></div>` : ''}
619
+ ${uniqueProps.length > 0 ? `
620
+ <div class="call-props">
621
+ <span class="props-label">属性使用:</span>
622
+ <div class="props-list">` +
623
+ uniqueProps.map(p => {
624
+ const propAccess = chain.propertyAccesses.find(pp => pp.accessChain.join('.') === p);
625
+ const propLine = propAccess?.line;
626
+ const propContext = (propAccess as any)?.codeContext || propAccess?.fullExpression || p;
627
+ return `<div class="prop-item">
628
+ <span class="prop-tag">${p}</span>
629
+ <div class="prop-context">
630
+ <span class="prop-line">行${propLine}:</span>
631
+ <code>${propContext}</code>
632
+ </div>
633
+ </div>`;
634
+ }).join('') + `
635
+ </div>
636
+ </div>` : ''}
637
+ </div>`;
638
+ }
639
+ callChainHtml += `
640
+ </div>`;
641
+ }
642
+
643
+ let html = `<!DOCTYPE html>
644
+ <html>
645
+ <head>
646
+ <meta charset="UTF-8">
647
+ <title>改动影响分析报告 - ${path.basename(scope.changedFile)}</title>
648
+ <style>
649
+ * { box-sizing: border-box; }
650
+ body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; max-width: 1200px; margin: 0 auto; padding: 40px 20px; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); min-height: 100vh; }
651
+ .container { background: white; border-radius: 20px; box-shadow: 0 20px 60px rgba(0,0,0,0.3); padding: 40px; }
652
+ h1 { color: #1a1a2e; margin: 0 0 30px 0; font-size: 2em; }
653
+ h2 { color: #333; margin: 30px 0 15px 0; font-size: 1.2em; border-bottom: 2px solid #eee; padding-bottom: 10px; }
654
+ .header { display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 30px; }
655
+ .badge { background: ${risk.color}; color: white; padding: 8px 20px; border-radius: 20px; font-weight: bold; font-size: 1.1em; }
656
+ .change-info { background: #f8f9fa; padding: 20px; border-radius: 12px; margin-bottom: 20px; }
657
+ .change-info p { margin: 8px 0; color: #555; }
658
+ .change-info strong { color: #333; }
659
+ .stats { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 20px; margin: 20px 0; }
660
+ .stat-card { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; padding: 25px; border-radius: 12px; text-align: center; }
661
+ .stat-card.green { background: linear-gradient(135deg, #11998e 0%, #38ef7d 100%); }
662
+ .stat-card.orange { background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%); }
663
+ .stat-card.blue { background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%); }
664
+ .stat-value { font-size: 2.5em; font-weight: bold; }
665
+ .stat-label { font-size: 0.9em; opacity: 0.9; margin-top: 5px; }
666
+ .risk-bar { font-size: 1.5em; letter-spacing: 2px; color: #666; }
667
+ .file-list { list-style: none; padding: 0; margin: 0; }
668
+ .file-item { background: #f8f9fa; padding: 20px; border-radius: 12px; margin: 12px 0; display: flex; justify-content: space-between; align-items: center; border-left: 4px solid #667eea; }
669
+ .file-item:hover { background: #f0f4ff; }
670
+ .file-name { font-weight: bold; color: #333; font-size: 1.1em; }
671
+ .file-type { color: #666; font-size: 0.9em; }
672
+ .file-refs { background: #667eea; color: white; padding: 5px 15px; border-radius: 20px; font-size: 0.9em; }
673
+ .recommendation { background: linear-gradient(135deg, #11998e 0%, #38ef7d 100%); color: white; padding: 20px; border-radius: 12px; margin-top: 20px; }
674
+ .summary { background: #f8f9fa; padding: 20px; border-radius: 12px; margin-top: 20px; text-align: center; font-size: 1.1em; color: #333; }
675
+ .footer { margin-top: 30px; text-align: center; color: #999; font-size: 0.85em; }
676
+ .call-chains { display: flex; flex-direction: column; gap: 15px; }
677
+ .call-chain-item { background: #f8f9fa; padding: 20px; border-radius: 12px; border-left: 4px solid #11998e; }
678
+ .call-chain-header { display: flex; justify-content: space-between; margin-bottom: 10px; }
679
+ .call-file { font-weight: bold; color: #333; font-size: 1.1em; }
680
+ .call-line { color: #666; }
681
+ .call-expression, .call-return { color: #555; margin: 5px 0; font-size: 0.95em; }
682
+ .call-expression code, .call-return code { background: #e9ecef; padding: 2px 8px; border-radius: 4px; color: #667eea; }
683
+ .call-props { margin-top: 12px; }
684
+ .props-label { color: #666; font-size: 0.9em; }
685
+ .props-list { display: flex; flex-direction: column; gap: 10px; margin-top: 8px; }
686
+ .prop-tag { background: linear-gradient(135deg, #11998e 0%, #38ef7d 100%); color: white; padding: 4px 12px; border-radius: 15px; font-size: 0.85em; display: inline-block; margin-bottom: 5px; }
687
+ .prop-item { background: #fff; padding: 10px; border-radius: 8px; border: 1px solid #e9ecef; }
688
+ .prop-context { margin-top: 5px; }
689
+ .prop-line { color: #666; font-size: 0.85em; }
690
+ .prop-context code { background: #f8f9fa; padding: 3px 8px; border-radius: 4px; color: #333; font-size: 0.9em; }
691
+ .code-context { background: #fff; border-radius: 8px; padding: 12px; margin: 10px 0; border: 1px solid #e9ecef; }
692
+ .code-label { color: #666; font-size: 0.85em; margin-bottom: 5px; }
693
+ .code-block { background: #1a1a2e; color: #00d4ff; padding: 12px; border-radius: 6px; margin: 0; font-size: 0.9em; overflow-x: auto; white-space: pre-wrap; word-break: break-all; }
694
+ .call-stack { display: flex; flex-direction: column; gap: 8px; }
695
+ .stack-node { background: #f8f9fa; padding: 15px; border-radius: 8px; border-left: 4px solid #11998e; }
696
+ .stack-root { border-left-color: #dc3545; }
697
+ .stack-header { display: flex; align-items: center; gap: 10px; margin-bottom: 5px; }
698
+ .stack-tag { font-size: 1.2em; }
699
+ .stack-name { font-weight: bold; color: #333; font-size: 1.1em; }
700
+ .stack-type { color: white; padding: 2px 8px; border-radius: 10px; font-size: 0.75em; background: #667eea; }
701
+ .stack-root-badge { background: #dc3545; color: white; padding: 2px 8px; border-radius: 10px; font-size: 0.75em; margin-left: 5px; }
702
+ .stack-location { color: #666; font-size: 0.9em; margin-left: 30px; }
703
+ .stack-call { margin-top: 8px; margin-left: 30px; }
704
+ .stack-call code { background: #e9ecef; padding: 2px 6px; border-radius: 4px; color: #667eea; font-size: 0.85em; }
705
+ .stack-children { margin-left: 30px; margin-top: 10px; display: flex; flex-direction: column; gap: 8px; }
706
+ .stack-depth { margin-top: 15px; padding: 10px; background: #f0f4ff; border-radius: 8px; font-size: 0.9em; color: #666; }
707
+ .stack-depth strong { color: #333; }
708
+ .type-result-ok { background: #f0f9f4; border: 1px solid #11998e; border-radius: 8px; padding: 15px; display: flex; flex-direction: column; gap: 5px; }
709
+ .type-result-warn { background: #fff5f5; border: 1px solid #f5576c; border-radius: 8px; padding: 15px; }
710
+ .type-result-warn p { margin: 0; }
711
+ .type-result-warn strong { color: #f5576c; }
712
+ .type-result-warn .meta, .type-result-ok .meta { font-size: 0.85em; color: #666; }
713
+ .type-list { display: flex; flex-direction: column; gap: 10px; margin-top: 15px; }
714
+ .type-item { background: #f8f9fa; border-radius: 8px; padding: 15px; border-left: 4px solid #f5576c; }
715
+ .type-location { display: flex; justify-content: space-between; margin-bottom: 10px; }
716
+ .type-location .file { font-weight: bold; color: #333; }
717
+ .confidence.high { background: #dc3545; color: white; padding: 2px 8px; border-radius: 10px; font-size: 0.8em; }
718
+ .confidence.medium { background: #ffc107; color: white; padding: 2px 8px; border-radius: 10px; font-size: 0.8em; }
719
+ .confidence.low { background: #6c757d; color: white; padding: 2px 8px; border-radius: 10px; font-size: 0.8em; }
720
+ .type-details { display: flex; flex-direction: column; gap: 5px; font-size: 0.95em; }
721
+ .type-details code { background: #e9ecef; padding: 2px 6px; border-radius: 4px; color: #667eea; }
722
+ .reason { color: #f5576c; margin-top: 5px; }
723
+ </style>
724
+ </head>
725
+ <body>
726
+ <div class="container">
727
+ <div class="header">
728
+ <h1>📊 改动影响范围分析报告</h1>
729
+ <div class="badge">${risk.label}</div>
730
+ </div>
731
+
732
+ <div class="change-info">
733
+ <p><strong>📁 改动文件:</strong> ${path.basename(scope.changedFile)}</p>
734
+ ${scope.changedSymbol ? `<p><strong>🔤 改动符号:</strong> ${scope.changedSymbol}</p>` : ''}
735
+ <p><strong>📝 改动类型:</strong> ${scope.changeType === 'modify' ? '修改' : scope.changeType === 'delete' ? '删除' : scope.changeType}</p>
736
+ <p><strong>⏰ 分析时间:</strong> ${new Date(scope.timestamp).toLocaleString('zh-CN')}</p>
737
+ </div>
738
+
739
+ <div class="risk-bar">影响程度: ${risk.bar} (${scope.stats.impactScore}分)</div>
740
+
741
+ <h2>📈 影响范围统计</h2>
742
+ <div class="stats">
743
+ <div class="stat-card green">
744
+ <div class="stat-value">${scope.stats.totalAffectedFiles}</div>
745
+ <div class="stat-label">受影响文件</div>
746
+ </div>
747
+ <div class="stat-card blue">
748
+ <div class="stat-value">${scope.stats.directReferences}</div>
749
+ <div class="stat-label">直接引用</div>
750
+ </div>
751
+ <div class="stat-card orange">
752
+ <div class="stat-value">${scope.stats.callSites}</div>
753
+ <div class="stat-label">调用点</div>
754
+ </div>
755
+ </div>
756
+
757
+ ${callChainHtml}
758
+
759
+ ${callStackView && callStackView.depth > 0 ? `
760
+ <h2>📞 调用栈视图</h2>
761
+ <div class="call-stack">
762
+ ${renderCallStackHtml(callStackView.root)}
763
+ <div class="stack-depth">深度: <strong>${callStackView.depth}</strong> 层</div>
764
+ </div>
765
+ ` : ''}
766
+
767
+ ${typeFlowResult && typeFlowResult.method ? `
768
+ ${typeFlowResult.hasIncompatibilities ? `
769
+ <h2>🔬 类型流分析 ⚠️</h2>
770
+ <div class="type-result-warn">
771
+ <p>检测到 <strong>${typeFlowResult.incompatibilities.length}</strong> 处类型不兼容</p>
772
+ <p class="meta">分析方法: ${typeFlowResult.method} | 可信度: ${typeFlowResult.confidence}</p>
773
+ </div>
774
+ <div class="type-list">
775
+ ${typeFlowResult.incompatibilities.slice(0, 5).map((item: any) => `
776
+ <div class="type-item">
777
+ <div class="type-location">
778
+ <span class="file">📄 ${path.basename(item.file)}:${item.line}</span>
779
+ <span class="confidence ${item.confidence || typeFlowResult.confidence}">${typeFlowResult.confidence}</span>
780
+ </div>
781
+ <div class="type-details">
782
+ ${item.expression ? `<div>调用: <code>${item.expression}</code></div>` : ''}
783
+ ${item.reason ? `<div class="reason">原因: ${item.reason}</div>` : ''}
784
+ </div>
785
+ </div>`).join('')}
786
+ </div>
787
+ ` : `
788
+ <h2>🔬 类型流分析</h2>
789
+ <div class="type-result-ok">
790
+ <span>✅ 未检测到类型不兼容</span>
791
+ <span class="meta">分析方法: ${typeFlowResult.method} | 可信度: ${typeFlowResult.confidence}</span>
792
+ </div>
793
+ `}
794
+ ` : ''}
795
+
796
+ <h2>📁 受影响的文件</h2>
797
+ <ul class="file-list">`;
798
+
799
+ for (const af of scope.affectedFiles.slice(0, 20)) {
800
+ const fileTypeText = fileTypeMap[af.category] || af.category || '其他';
801
+ html += `
802
+ <li class="file-item">
803
+ <div>
804
+ <div class="file-name">${path.basename(af.file)}</div>
805
+ <div class="file-type">${af.file}</div>
806
+ <div class="file-type">类型: ${fileTypeText}</div>
807
+ </div>
808
+ <div class="file-refs">${af.references.length} 处引用</div>
809
+ </li>`;
810
+ }
811
+
812
+ html += `
813
+ </ul>`;
814
+
815
+ // Summary recommendation
816
+ let summaryText = '✅ 此改动影响很小,可以正常发布';
817
+ if (scope.stats.totalAffectedFiles <= 1 && scope.stats.callSites <= 2) {
818
+ summaryText = '✅ 此改动影响很小,可以正常发布';
819
+ } else if (scope.stats.totalAffectedFiles <= 5) {
820
+ summaryText = '⚠️ 此改动有少量影响,发布前建议检查相关文件';
821
+ } else {
822
+ summaryText = '🔴 此改动影响范围较大,建议进行充分测试后再发布';
823
+ }
824
+
825
+ html += `
826
+ <div class="summary">${summaryText}</div>
827
+ <div class="footer">由 Blast Radius Analyzer 自动生成</div>
828
+ </div>
829
+ </body>
830
+ </html>`;
831
+
832
+ return html;
833
+ }
834
+
835
+ // ─── Main ────────────────────────────────────────────────────────────────────
836
+
837
+ async function main(): Promise<void> {
838
+ const args = parseArgs(process.argv);
839
+
840
+ if (args.changes.length === 0) {
841
+ console.error('❌ Error: No changes specified. Use --change <file>');
842
+ console.error(' Run with --help for usage information');
843
+ process.exit(1);
844
+ }
845
+
846
+ // 初始化缓存
847
+ const cache = new AnalysisCache(args.projectRoot);
848
+
849
+ if (args.clearCache) {
850
+ cache.clear();
851
+ console.log('🗑️ Cache cleared');
852
+ }
853
+
854
+ const cacheStats = cache.getStats();
855
+ console.log('💥 Blast Radius Analyzer');
856
+ console.log(` Project: ${args.projectRoot}`);
857
+ console.log(` Changes: ${args.changes.map((c) => `${c.file}${c.symbol ? `#${c.symbol}` : ''} (${c.type})`).join(', ')}`);
858
+ if (args.useCache) {
859
+ console.log(` Cache: ${cacheStats.entries} entries, ${cacheStats.files} files tracked`);
860
+ } else {
861
+ console.log(' Cache: disabled');
862
+ }
863
+ console.log('');
864
+
865
+ const config: AnalyzerConfig = {
866
+ projectRoot: args.projectRoot,
867
+ tsConfigPath: args.tsConfig,
868
+ maxDepth: args.maxDepth,
869
+ includeNodeModules: false,
870
+ includeTests: args.includeTests,
871
+ criticalPatterns: [
872
+ '**/main.ts',
873
+ '**/index.ts',
874
+ '**/App.tsx',
875
+ '**/App.ts',
876
+ '**/routes/**',
877
+ '**/entry*.ts',
878
+ ],
879
+ ignorePatterns: ['**/*.spec.ts', '**/*.test.ts', '**/__tests__/**'],
880
+ verbose: args.verbose,
881
+ outputFormat: args.format,
882
+ };
883
+
884
+ const tracer = new ImpactTracer(args.projectRoot, args.tsConfig, config);
885
+
886
+ try {
887
+ console.log('🔍 Initializing symbol analyzer...');
888
+ await tracer.initialize();
889
+
890
+ // 检查是否有缓存的未变更文件
891
+ const changedFiles = args.changes.map(c => c.file);
892
+ const changedFilesList = cache.getChangedFiles(changedFiles);
893
+
894
+ if (args.useCache && changedFilesList.length === 0) {
895
+ console.log('📦 Using cached analysis results (no file changes detected)');
896
+ } else {
897
+ if (changedFilesList.length > 0 && args.useCache) {
898
+ console.log(`🔄 Re-analyzing ${changedFilesList.length} changed file(s)`);
899
+ }
900
+ console.log('🔬 Analyzing blast radius...');
901
+ }
902
+
903
+ const scopes = await Promise.all(
904
+ args.changes.map((change) =>
905
+ tracer.traceImpact(change.file, change.symbol, change.type)
906
+ )
907
+ );
908
+
909
+ // 更新缓存
910
+ if (args.useCache) {
911
+ for (const change of args.changes) {
912
+ cache.updateFileState(change.file);
913
+ }
914
+ }
915
+
916
+ // 合并结果(简化处理,只显示第一个)
917
+ const primaryScope = scopes[0];
918
+
919
+ // 调用链分析 - 获取属性访问详情
920
+ let callChains: CallChainAnalysis[] = [];
921
+ if (primaryScope.changedSymbol) {
922
+ try {
923
+ const propTracker = new PropertyAccessTracker(args.projectRoot, args.tsConfig);
924
+ // 只分析受影响的文件
925
+ const affectedFileNames = primaryScope.affectedFiles
926
+ .map(af => path.basename(af.file))
927
+ .filter((name, idx, arr) => arr.indexOf(name) === idx); // 去重
928
+
929
+ const propResults = propTracker.analyzeFunctionCalls(
930
+ primaryScope.changedSymbol,
931
+ affectedFileNames
932
+ );
933
+ callChains = propResults.map(r => ({
934
+ file: r.file,
935
+ line: r.line,
936
+ callExpression: r.callExpression,
937
+ returnedTo: r.returnedTo,
938
+ codeContext: (r as any).codeContext || r.callExpression,
939
+ propertyAccesses: r.propertyAccesses.map(p => ({
940
+ accessChain: p.accessChain,
941
+ line: p.line,
942
+ fullExpression: p.fullExpression,
943
+ codeContext: (p as any).codeContext || p.fullExpression,
944
+ })),
945
+ }));
946
+ } catch (e) {
947
+ // 忽略错误
948
+ }
949
+ }
950
+
951
+ // 下游链路详情
952
+ const downstreamChain = (primaryScope as any).downstreamChain || [];
953
+
954
+ // 调用栈视图 - 从改动点向上追踪到入口
955
+ let callStackView: { root: any; depth: number; path: string[] } | null = null;
956
+ if (primaryScope.changedSymbol && primaryScope.symbolInfo) {
957
+ try {
958
+ const stackBuilder = new CallStackBuilder(args.projectRoot, args.tsConfig);
959
+ stackBuilder.addSourceFiles([`${args.projectRoot}/**/*.ts`, `${args.projectRoot}/**/*.tsx`]);
960
+ callStackView = stackBuilder.buildCallStack(
961
+ primaryScope.changedSymbol,
962
+ primaryScope.changedFile
963
+ );
964
+ } catch (e) {
965
+ // 忽略调用栈构建错误
966
+ }
967
+ }
968
+
969
+ // 类型流分析 - 检测类型不兼容
970
+ let typeFlowResult: any = null;
971
+ let dataFlowResult: any = null;
972
+ if (primaryScope.changedSymbol && primaryScope.symbolInfo) {
973
+ try {
974
+ const typeFlowAnalyzer = new TypeFlowAnalyzer(args.projectRoot, args.tsConfig);
975
+ typeFlowResult = typeFlowAnalyzer.analyzeTypeFlow(
976
+ primaryScope.changedSymbol,
977
+ primaryScope.changedFile
978
+ );
979
+ } catch (e) {
980
+ // 忽略类型流分析错误
981
+ }
982
+ try {
983
+ const dataFlowAnalyzer = new DataFlowAnalyzer(args.projectRoot, args.tsConfig);
984
+ dataFlowResult = dataFlowAnalyzer.analyzeDataFlow(
985
+ primaryScope.changedSymbol,
986
+ primaryScope.changedFile
987
+ );
988
+ } catch (e) {
989
+ // 忽略数据流分析错误
990
+ }
991
+ }
992
+
993
+ // 如果是 graph 格式,生成依赖图
994
+ if (args.format === 'graph') {
995
+ const graphBuilder = new DependencyGraphBuilder();
996
+ const graph = graphBuilder.build(
997
+ primaryScope.symbolInfo!,
998
+ primaryScope.categorized.calls.concat(
999
+ primaryScope.categorized.types,
1000
+ primaryScope.categorized.exports,
1001
+ primaryScope.categorized.properties
1002
+ ),
1003
+ primaryScope.changedFile
1004
+ );
1005
+ const graphHtml = graphBuilder.generateInteractiveHtml(
1006
+ graph,
1007
+ path.basename(primaryScope.changedFile)
1008
+ );
1009
+
1010
+ const graphOutput = args.output || '/tmp/blast-radius-graph.html';
1011
+ fs.writeFileSync(graphOutput, graphHtml, 'utf-8');
1012
+ console.log(`\n✅ Dependency graph written to: ${graphOutput}`);
1013
+ console.log(' Open in a browser to view the interactive graph');
1014
+ return;
1015
+ }
1016
+
1017
+ // 输出
1018
+ let output: string;
1019
+ let exitCode = 0;
1020
+
1021
+ // CI/CD 阈值检查
1022
+ if (args.threshold) {
1023
+ const alerts: string[] = [];
1024
+
1025
+ if (args.threshold.files && primaryScope.stats.totalAffectedFiles > args.threshold.files) {
1026
+ alerts.push(`受影响文件数 ${primaryScope.stats.totalAffectedFiles} 超过阈值 ${args.threshold.files}`);
1027
+ }
1028
+ if (args.threshold.score && primaryScope.stats.impactScore > args.threshold.score) {
1029
+ alerts.push(`影响分数 ${primaryScope.stats.impactScore} 超过阈值 ${args.threshold.score}`);
1030
+ }
1031
+ if (args.threshold.typeErrors && typeFlowResult && typeFlowResult.incompatibilities.length > args.threshold.typeErrors) {
1032
+ alerts.push(`类型不兼容 ${typeFlowResult.incompatibilities.length} 超过阈值 ${args.threshold.typeErrors}`);
1033
+ }
1034
+
1035
+ if (alerts.length > 0) {
1036
+ console.error('\n🚨 CI/CD 阈值告警:');
1037
+ for (const alert of alerts) {
1038
+ console.error(` ⚠️ ${alert}`);
1039
+ }
1040
+ exitCode = 2; // 阈值告警使用退出码 2
1041
+ }
1042
+ }
1043
+
1044
+ switch (args.format) {
1045
+ case 'json':
1046
+ output = JSON.stringify({ ...extractSerializableScope(primaryScope), callChains, callStackView, typeFlowResult }, null, 2);
1047
+ break;
1048
+ case 'html':
1049
+ output = formatHtml(primaryScope, callChains, callStackView, typeFlowResult, dataFlowResult);
1050
+ break;
1051
+ default:
1052
+ output = formatText(primaryScope, callChains, callStackView, typeFlowResult, dataFlowResult);
1053
+ }
1054
+
1055
+ if (args.output) {
1056
+ fs.writeFileSync(args.output, output, 'utf-8');
1057
+ console.log(`\n✅ Report written to: ${args.output}`);
1058
+ } else {
1059
+ console.log('\n' + output);
1060
+ }
1061
+
1062
+ if (exitCode > 0) {
1063
+ process.exit(exitCode);
1064
+ }
1065
+ } catch (error) {
1066
+ console.error('❌ Analysis failed:', error);
1067
+ process.exit(1);
1068
+ }
1069
+ }
1070
+
1071
+ main();